├── .env ├── .env.example ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── server ├── index.js └── package.json └── src ├── assets └── images │ ├── dropdown.svg │ ├── pass.svg │ └── user.svg ├── axios.js ├── components ├── ConfirmModal.js ├── ConfirmModal.scss ├── CustomScrollbars.js ├── CustomScrollbars.scss ├── CustomToast.js ├── CustomToast.scss ├── Formating │ ├── FormattedDate.js │ └── index.js ├── Input │ ├── DatePicker.js │ ├── DatePicker.scss │ ├── InputSuggest.js │ ├── InputSuggest.scss │ └── index.js ├── Navigator.js └── Navigator.scss ├── config.js ├── containers ├── App.js ├── App.scss ├── Header │ ├── Header.js │ ├── Header.scss │ └── menuApp.js └── System │ ├── ProductManage.js │ ├── RegisterPackageGroupOrAcc.js │ └── UserManage.js ├── hoc ├── IntlProviderWrapper.js └── authentication.js ├── index.js ├── redux.js ├── routes ├── Home.js ├── Login.js ├── Login.scss └── System.js ├── serviceWorker.js ├── services ├── adminService.js └── index.js ├── store ├── actions │ ├── actionTypes.js │ ├── adminActions.js │ ├── appActions.js │ ├── index.js │ └── userActions.js └── reducers │ ├── adminReducer.js │ ├── appReducer.js │ ├── rootReducer.js │ └── userReducer.js ├── styles ├── _base.scss ├── _form.scss ├── _variables.scss ├── common.scss └── styles.scss ├── translations ├── en.json └── vi.json └── utils ├── CommonUtils.js ├── KeyCodeUtils.js ├── LanguageUtils.js ├── ToastUtil.js ├── constant.js └── index.js /.env: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | NODE_ENV=development 3 | REACT_APP_BACKEND_URL=http://localhost:8080 4 | 5 | #The base URL for all locations. If your app is served from a sub-directory on your server, you'll want to set 6 | #this to the sub-directory. A properly formatted basename should have a leading slash, but no trailing slash. 7 | REACT_APP_ROUTER_BASE_NAME= -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | NODE_ENV=development 3 | REACT_APP_BACKEND_URL=http://localhost:8080 4 | 5 | #The base URL for all locations. If your app is served from a sub-directory on your server, you'll want to set 6 | #this to the sub-directory. A properly formatted basename should have a leading slash, but no trailing slash. 7 | REACT_APP_ROUTER_BASE_NAME= -------------------------------------------------------------------------------- /.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 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-app-hoi-dan-it", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@formatjs/intl-pluralrules": "^3.5.6", 7 | "@formatjs/intl-relativetimeformat": "^7.3.6", 8 | "@fortawesome/fontawesome-free-webfonts": "^1.0.9", 9 | "axios": "^0.21.1", 10 | "bootstrap": "^5.0.1", 11 | "connected-react-router": "^6.9.1", 12 | "lodash": "^4.17.21", 13 | "moment": "^2.29.0", 14 | "node-sass": "^6.0.0", 15 | "react": "^17.0.2", 16 | "react-auth-wrapper": "^1.0.0", 17 | "react-autosuggest": "^10.1.0", 18 | "react-bootstrap-table-next": "^4.0.3", 19 | "react-bootstrap-table2-filter": "^1.3.3", 20 | "react-custom-scrollbars": "^4.2.1", 21 | "react-dom": "^17.0.2", 22 | "react-flatpickr": "^3.10.7", 23 | "react-intl": "^5.20.2", 24 | "react-redux": "^7.2.4", 25 | "react-router": "^5.2.0", 26 | "react-router-dom": "^5.2.0", 27 | "react-scripts": "^4.0.3", 28 | "react-toastify": "^5.5.0", 29 | "reactstrap": "^8.9.0", 30 | "redux": "^4.1.0", 31 | "redux-auth-wrapper": "^2.1.0", 32 | "redux-logger": "^3.0.6", 33 | "redux-persist": "^5.10.0", 34 | "redux-state-sync": "^2.1.0", 35 | "redux-thunk": "^2.3.0", 36 | "typescript": "^4.3.2" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": "react-app" 46 | }, 47 | "browserslist": { 48 | "production": [ 49 | ">0.2%", 50 | "not dead", 51 | "not op_mini all" 52 | ], 53 | "development": [ 54 | "last 1 chrome version", 55 | "last 1 firefox version", 56 | "last 1 safari version" 57 | ] 58 | } 59 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haryphamdev/Frontend-React.JS-QuickStart/b0f772df40c293463f1958c50c13c71534c3c04e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | HoiDanIT React App 24 | 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | 4 | const app = express(); 5 | 6 | const buildDir = path.join(__dirname, '../build'); 7 | console.log('Using files in ' + buildDir); 8 | 9 | const subDir = '/'; 10 | const logRequests = false; 11 | 12 | if (subDir === '/') { 13 | console.log('The server config assuming it is serving at the server root. You can control this with the `subDir` variable in index.js.'); 14 | } else { 15 | console.log('The server config assuming it is serving at \'' + subDir + '\'.'); 16 | } 17 | 18 | if (logRequests) { 19 | console.log('The server will log all incoming request. It\'s not recommended for production use.'); 20 | } 21 | 22 | // Serve the static files from the React app 23 | app.use(subDir, express.static(buildDir)); 24 | // Handles any requests that don't match the ones above 25 | app.get('*', (req, res) => { 26 | if (logRequests) { 27 | console.log(req.method + ' ' + req.url); 28 | } 29 | res.sendFile(path.join(buildDir, 'index.html')); 30 | }); 31 | 32 | const port = process.env.PORT || 3000; 33 | app.listen(port); 34 | 35 | console.log('React.JS App is running on the port ' + port); -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hoidanit-frontend-server", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node index.js" 7 | }, 8 | "dependencies": { 9 | "express": "^4.16.4" 10 | } 11 | } -------------------------------------------------------------------------------- /src/assets/images/dropdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/pass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/images/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import _ from 'lodash'; 3 | import config from './config'; 4 | 5 | const instance = axios.create({ 6 | baseURL: process.env.REACT_APP_BACKEND_URL, 7 | withCredentials: true 8 | }); 9 | 10 | const createError = (httpStatusCode, statusCode, errorMessage, problems, errorCode = '') => { 11 | const error = new Error(); 12 | error.httpStatusCode = httpStatusCode; 13 | error.statusCode = statusCode; 14 | error.errorMessage = errorMessage; 15 | error.problems = problems; 16 | error.errorCode = errorCode + ""; 17 | return error; 18 | }; 19 | 20 | export const isSuccessStatusCode = (s) => { 21 | // May be string or number 22 | const statusType = typeof s; 23 | return (statusType === 'number' && s === 0) || (statusType === 'string' && s.toUpperCase() === 'OK'); 24 | }; 25 | 26 | instance.interceptors.response.use( 27 | (response) => { 28 | // Thrown error for request with OK status code 29 | const { data } = response; 30 | if (data.hasOwnProperty('s') && !isSuccessStatusCode(data['s']) && data.hasOwnProperty('errmsg')) { 31 | return Promise.reject(createError(response.status, data['s'], data['errmsg'], null, data['errcode'] ? data['errcode'] : "")); 32 | } 33 | 34 | // Return direct data to callback 35 | if (data.hasOwnProperty('s') && data.hasOwnProperty('d')) { 36 | return data['d']; 37 | } 38 | // Handle special case 39 | if (data.hasOwnProperty('s') && _.keys(data).length === 1) { 40 | return null; 41 | } 42 | return response.data; 43 | }, 44 | (error) => { 45 | const { response } = error; 46 | if (response == null) { 47 | return Promise.reject(error); 48 | } 49 | 50 | const { data } = response; 51 | 52 | if (data.hasOwnProperty('s') && data.hasOwnProperty('errmsg')) { 53 | return Promise.reject(createError(response.status, data['s'], data['errmsg'])); 54 | } 55 | 56 | if (data.hasOwnProperty('code') && data.hasOwnProperty('message')) { 57 | return Promise.reject(createError(response.status, data['code'], data['message'], data['problems'])); 58 | } 59 | 60 | return Promise.reject(createError(response.status)); 61 | } 62 | ); 63 | 64 | export default instance; 65 | -------------------------------------------------------------------------------- /src/components/ConfirmModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { connect } from 'react-redux'; 4 | import { Modal } from 'reactstrap'; 5 | 6 | import './ConfirmModal.scss'; 7 | import * as actions from "../store/actions"; 8 | import { KeyCodeUtils } from "../utils"; 9 | 10 | class ConfirmModal extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | this.acceptBtnRef = React.createRef(); 15 | } 16 | 17 | initialState = { 18 | }; 19 | 20 | state = { 21 | ...this.initialState 22 | }; 23 | 24 | componentDidMount() { 25 | document.addEventListener('keydown', this.handlerKeyDown); 26 | } 27 | 28 | componentWillUnmount() { 29 | document.removeEventListener('keydown', this.handlerKeyDown); 30 | } 31 | 32 | handlerKeyDown = (event) => { 33 | const keyCode = event.which || event.keyCode; 34 | if (keyCode === KeyCodeUtils.ENTER) { 35 | if (!this.acceptBtnRef.current || this.acceptBtnRef.current.disabled) return; 36 | this.acceptBtnRef.current.click(); 37 | } 38 | } 39 | 40 | onAcceptBtnClick = () => { 41 | const { contentOfConfirmModal } = this.props; 42 | if (contentOfConfirmModal.handleFunc) { 43 | contentOfConfirmModal.handleFunc(contentOfConfirmModal.dataFunc); 44 | } 45 | this.onClose(); 46 | } 47 | 48 | onClose = () => { 49 | this.props.setContentOfConfirmModal({ 50 | isOpen: false, 51 | messageId: "", 52 | handleFunc: null, 53 | dataFunc: null 54 | }); 55 | } 56 | 57 | render() { 58 | const { contentOfConfirmModal } = this.props; 59 | 60 | return ( 61 | 62 |
63 |
64 | 65 |
66 |
67 | 70 |
71 |
72 | 73 |
74 |
75 |
76 |
77 | 78 |
79 | 80 |
81 | 82 |
83 |
84 | 87 | 90 |
91 |
92 |
93 |
94 |
95 |
96 | ); 97 | } 98 | 99 | } 100 | 101 | const mapStateToProps = state => { 102 | return { 103 | lang: state.app.language, 104 | contentOfConfirmModal: state.app.contentOfConfirmModal 105 | }; 106 | }; 107 | 108 | const mapDispatchToProps = dispatch => { 109 | return { 110 | setContentOfConfirmModal: (contentOfConfirmModal) => dispatch(actions.setContentOfConfirmModal(contentOfConfirmModal)) 111 | }; 112 | }; 113 | 114 | export default connect(mapStateToProps, mapDispatchToProps)(ConfirmModal); 115 | -------------------------------------------------------------------------------- /src/components/ConfirmModal.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/common"; 2 | 3 | .confirm-modal { 4 | &.modal-dialog { 5 | @media (min-width: 766px) { 6 | min-width: 650px; 7 | } 8 | } 9 | 10 | hr { 11 | border: none; 12 | border-bottom: 1px solid $border;; 13 | width: 100%; 14 | margin: 10px 15px 5px 15px; 15 | } 16 | 17 | .btn-container { 18 | margin-top: 10px; 19 | .btn { 20 | width: 100px; 21 | &:first-child { 22 | margin-right: 10px; 23 | } 24 | } 25 | } 26 | 27 | .custom-form-group { 28 | margin: 5px 0; 29 | 30 | .custom-form-control.readonly { 31 | min-height: 28px; 32 | height: auto; 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/components/CustomScrollbars.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Scrollbars } from 'react-custom-scrollbars'; 3 | 4 | import './CustomScrollbars.scss'; 5 | 6 | class CustomScrollbars extends Component { 7 | 8 | ref = React.createRef(); 9 | 10 | getScrollLeft =()=>{ 11 | const scrollbars = this.ref.current; 12 | return scrollbars.getScrollLeft(); 13 | } 14 | getScrollTop =()=>{ 15 | const scrollbars = this.ref.current; 16 | return scrollbars.getScrollTop(); 17 | } 18 | 19 | scrollToBottom = () => { 20 | if (!this.ref || !this.ref.current) { 21 | return; 22 | } 23 | const scrollbars = this.ref.current; 24 | const targetScrollTop = scrollbars.getScrollHeight(); 25 | this.scrollTo(targetScrollTop); 26 | }; 27 | 28 | scrollTo = (targetTop) => { 29 | const { quickScroll } = this.props; 30 | if (!this.ref || !this.ref.current) { 31 | return; 32 | } 33 | const scrollbars = this.ref.current; 34 | const originalTop = scrollbars.getScrollTop(); 35 | let iteration = 0; 36 | 37 | const scroll = () => { 38 | iteration++; 39 | if (iteration > 30) { 40 | return; 41 | } 42 | scrollbars.scrollTop(originalTop + (targetTop - originalTop) / 30 * iteration); 43 | 44 | if (quickScroll && quickScroll === true) { 45 | scroll(); 46 | } else { 47 | setTimeout(() => { 48 | scroll(); 49 | }, 20); 50 | } 51 | }; 52 | 53 | scroll(); 54 | }; 55 | 56 | renderTrackHorizontal = (props) => { 57 | return ( 58 |
59 | ); 60 | }; 61 | 62 | renderTrackVertical = (props) => { 63 | return ( 64 |
65 | ); 66 | }; 67 | 68 | renderThumbHorizontal = (props) => { 69 | return ( 70 |
71 | ); 72 | }; 73 | 74 | renderThumbVertical = (props) => { 75 | return ( 76 |
77 | ); 78 | }; 79 | 80 | renderNone = (props) => { 81 | return ( 82 |
83 | ); 84 | }; 85 | 86 | render() { 87 | const { className, disableVerticalScroll, disableHorizontalScroll, children,...otherProps } = this.props; 88 | return ( 89 | 101 | {children} 102 | 103 | ); 104 | } 105 | } 106 | 107 | export default CustomScrollbars; -------------------------------------------------------------------------------- /src/components/CustomScrollbars.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/common.scss"; 2 | 3 | .custom-scrollbar { 4 | .thumb-horizontal { 5 | background-color: rgba($bg-scrollbar, 0.4); 6 | box-shadow: inset 1px 1px 0 rgba($bg-scrollbar, 0.10), inset 0 -1px 0 rgba($bg-scrollbar, 0.07); 7 | 8 | &:hover { 9 | background-color: rgba($bg-scrollbar, 0.8); 10 | box-shadow: inset 1px 1px 1px rgba($bg-scrollbar, 0.50); 11 | } 12 | 13 | &:active { 14 | background-color: rgba($bg-scrollbar, 0.9); 15 | box-shadow: inset 1px 1px 3px rgba($bg-scrollbar, 0.6); 16 | } 17 | } 18 | 19 | .thumb-vertical { 20 | background-color: rgba($bg-scrollbar, 0.4); 21 | box-shadow: inset 1px 1px 0 rgba($bg-scrollbar, 0.10), inset 0 -1px 0 rgba($bg-scrollbar, 0.07); 22 | 23 | &:hover { 24 | background-color: rgba($bg-scrollbar, 0.8); 25 | box-shadow: inset 1px 1px 1px rgba($bg-scrollbar, 0.50); 26 | } 27 | 28 | &:active { 29 | background-color: rgba($bg-scrollbar, 0.9); 30 | box-shadow: inset 1px 1px 3px rgba($bg-scrollbar, 0.6); 31 | } 32 | } 33 | 34 | 35 | &:hover { 36 | 37 | > .track-horizontal, > .track-vertical { 38 | opacity: 1 !important; 39 | 40 | > .thumb-horizontal, > .thumb-vertical { 41 | opacity: 1; 42 | } 43 | } 44 | } 45 | 46 | .track-horizontal { 47 | right: 0; 48 | bottom: 0; 49 | left: 0; 50 | height: 5px !important; 51 | &:hover, &:active { 52 | height: 10px !important; 53 | } 54 | } 55 | 56 | .track-vertical { 57 | right: 0; 58 | bottom: 0; 59 | top: 0; 60 | width: 5px !important; 61 | &:hover, &:active { 62 | width: 10px !important; 63 | } 64 | } 65 | 66 | .thumb-horizontal { 67 | cursor: pointer; 68 | border-radius: 10px; 69 | background-clip: padding-box; 70 | min-width: 0px; 71 | 72 | } 73 | 74 | .thumb-vertical { 75 | cursor: pointer; 76 | border-radius: 10px; 77 | background-clip: padding-box; 78 | min-height: 0px; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/components/CustomToast.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { FormattedMessage, FormattedTime } from 'react-intl'; 3 | 4 | import CustomScrollBar from '../components/CustomScrollbars'; 5 | 6 | import './CustomToast.scss'; 7 | 8 | class CustomToast extends Component { 9 | 10 | render() { 11 | const { titleId, message, messageId, time } = this.props; 12 | return ( 13 | 14 |
15 |
16 | {time && ( 17 | 18 | 19 | 20 | )} 21 | 22 | 23 |
24 | { 25 | (message && typeof message === 'object') ? 26 | 27 | { 28 | message.map((msg, index) => { 29 | return ( 30 | 31 |
{msg}
32 |
33 | ) 34 | }) 35 | } 36 |
: 37 |
38 | {message ? message : (messageId ? () : null)} 39 |
40 | } 41 |
42 |
43 | ); 44 | } 45 | } 46 | 47 | export class CustomToastCloseButton extends Component { 48 | 49 | render() { 50 | return ( 51 | 54 | ); 55 | } 56 | } 57 | 58 | export default CustomToast; -------------------------------------------------------------------------------- /src/components/CustomToast.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/common.scss"; 2 | 3 | .toast-container { 4 | .toast-item { 5 | white-space: pre-wrap; 6 | box-shadow: 0 0 5px 5px $box-shadow-color; 7 | -webkit-box-shadow: 0 0 5px 5px $box-shadow-color; 8 | 9 | &.Toastify__toast--success { 10 | background: $common-green; 11 | } 12 | 13 | &.Toastify__toast--error { 14 | background: $common-red; 15 | } 16 | 17 | &.Toastify__toast--warn { 18 | background: $colormain; 19 | } 20 | 21 | &.Toastify__toast--info { 22 | background: $colormain; 23 | } 24 | 25 | .toast-close { 26 | position: absolute; 27 | right: 10px; 28 | top:10px; 29 | color: $common-text-color; 30 | font-size: $base-size; 31 | padding: 0; 32 | cursor: pointer; 33 | background: transparent; 34 | border: 0; 35 | -webkit-appearance: none; 36 | transition: opacity 0.2s ease-out; 37 | } 38 | 39 | .toast-item-body { 40 | color: $common-text-color; 41 | display: block; 42 | flex: none; 43 | width: 100%; 44 | 45 | .toast-title { 46 | font-weight: bold; 47 | font-size: $base-size; 48 | margin: 0 20px 5px 0; 49 | 50 | .fixed-scroll-bar { 51 | height: 50px; 52 | } 53 | .date { 54 | float: right; 55 | font-size: $base-size - 2px; 56 | vertical-align: middle; 57 | margin-right: 5px; 58 | margin-bottom: 0; 59 | padding: 2px 5px; 60 | } 61 | 62 | i { 63 | position: relative; 64 | margin-right: 3px; 65 | } 66 | } 67 | 68 | .toast-content { 69 | font-size: $base-size - 2px; 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/components/Formating/FormattedDate.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | 4 | /** For valid format please see Moment format options */ 5 | const dateFormat = 'DD/MM/YYYY'; 6 | 7 | class FormattedDate extends Component { 8 | 9 | render() { 10 | const { format, value, ...otherProps } = this.props; 11 | var dFormat = format ? format : dateFormat; 12 | const formattedValue = value ? moment.utc(value).format(dFormat) : null; 13 | return ( 14 | {formattedValue} 15 | ); 16 | } 17 | } 18 | 19 | export default FormattedDate; -------------------------------------------------------------------------------- /src/components/Formating/index.js: -------------------------------------------------------------------------------- 1 | export { default as FormattedDate } from './FormattedDate'; -------------------------------------------------------------------------------- /src/components/Input/DatePicker.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Flatpickr from 'react-flatpickr'; 3 | import moment from 'moment'; 4 | 5 | import { KeyCodeUtils } from "../../utils"; 6 | import './DatePicker.scss'; 7 | 8 | // const CustomInput = ({ value, defaultValue, inputRef, onInputChange, onInputBlur, ...props }) => { 9 | // return ; 13 | // }; 14 | 15 | class DatePicker extends Component { 16 | 17 | flatpickrNode = null; 18 | 19 | nodeRef = element => { 20 | this.flatpickr = element && element.flatpickr; 21 | this.flatpickrNode = element && element.node; 22 | if (this.flatpickrNode) { 23 | this.flatpickrNode.addEventListener('blur', this.handleBlur); 24 | this.flatpickrNode.addEventListener('keydown', this.handlerKeyDown); 25 | } 26 | }; 27 | 28 | handlerKeyDown = (event) => { 29 | const keyCode = event.which || event.keyCode; 30 | if (keyCode === KeyCodeUtils.ENTER) { 31 | event.preventDefault(); 32 | const { onChange } = this.props; 33 | const value = event.target.value; 34 | 35 | // Take the blur event and process the string value 36 | const valueMoment = moment(value, 'DD/MM/YYYY'); 37 | onChange([valueMoment.toDate(), valueMoment.toDate()]); 38 | } 39 | } 40 | 41 | componentWillUnmount() { 42 | if (this.flatpickrNode) { 43 | this.flatpickrNode.removeEventListener('blur', this.handleBlur); 44 | this.flatpickrNode.removeEventListener('keydown', this.handlerKeyDown); 45 | } 46 | } 47 | 48 | handleBlur = (event) => { 49 | const { onChange } = this.props; 50 | const value = event.target.value; 51 | 52 | // Take the blur event and process the string value 53 | event.preventDefault(); 54 | const valueMoment = moment(value, 'DD/MM/YYYY'); 55 | onChange([valueMoment.toDate(), valueMoment.toDate()]); 56 | }; 57 | 58 | onOpen = () => { 59 | if (this.flatpickrNode) { 60 | this.flatpickrNode.blur(); 61 | } 62 | } 63 | 64 | close() { 65 | this.flatpickr.close(); 66 | } 67 | 68 | checkDateValue = (str, max) => { 69 | if (str.charAt(0) !== '0' || str === '00') { 70 | var num = parseInt(str); 71 | if (isNaN(num) || num <= 0 || num > max) num = 1; 72 | str = num > parseInt(max.toString().charAt(0)) && num.toString().length === 1 ? '0' + num : num.toString(); 73 | }; 74 | return str; 75 | } 76 | 77 | // autoFormatonBlur = (value) => { 78 | // var input = value; 79 | // var values = input.split('/').map(function (v, i) { 80 | // return v.replace(/\D/g, '') 81 | // }); 82 | // var output = ''; 83 | 84 | // if (values.length == 3) { 85 | // var year = values[2].length !== 4 ? parseInt(values[2]) + 2000 : parseInt(values[2]); 86 | // var month = parseInt(values[0]) - 1; 87 | // var day = parseInt(values[1]); 88 | // var d = new Date(year, month, day); 89 | // if (!isNaN(d)) { 90 | // //document.getElementById('result').innerText = d.toString(); 91 | // var dates = [d.getMonth() + 1, d.getDate(), d.getFullYear()]; 92 | // output = dates.map(function (v) { 93 | // v = v.toString(); 94 | // return v.length == 1 ? '0' + v : v; 95 | // }).join(' / '); 96 | // }; 97 | // }; 98 | // // this.value = output; 99 | // return output; 100 | // } 101 | 102 | autoFormatOnChange = (value, seperator) => { 103 | var input = value; 104 | 105 | let regexForDeleting = new RegExp(`\\D\\${seperator}$`); 106 | 107 | //if (/\D\/$/.test(input)) input = input.substr(0, input.length - 3); // dat.nt: Xóa thêm 1 ký tự nếu xóa dấu cách sau / (VD: 12 / 12 /=> 12 / 1) 108 | 109 | if (regexForDeleting.test(input)) input = input.substr(0, input.length - 3); 110 | 111 | var values = input.split(seperator).map(function (v) { 112 | return v.replace(/\D/g, '') 113 | }); 114 | 115 | if (values[0]) values[0] = this.checkDateValue(values[0], 31); 116 | if (values[1]) values[1] = this.checkDateValue(values[1], 12); 117 | var output = values.map(function (v, i) { 118 | return v.length === 2 && i < 2 ? v + ' ' + seperator + ' ' : v; 119 | }); 120 | return output.join('').substr(0, 14); 121 | } 122 | 123 | onInputChange = (e) => { 124 | if (this.DISPLAY_FORMAT === this.DATE_FORMAT_AUTO_FILL) { 125 | let converted = this.autoFormatOnChange(e.target.value, this.SEPERATOR); 126 | e.target.value = converted; 127 | } 128 | } 129 | 130 | onInputBlur = (e) => { 131 | } 132 | 133 | //dat.nt : Auto Fill cho dạng ngăn cách và format cụ thể (seperator có thể dc thay thế) 134 | SEPERATOR = "/"; 135 | DATE_FORMAT_AUTO_FILL = "d/m/Y"; // Format không thay đổi 136 | 137 | // dat.nt : Format ngày hiển thị 138 | DISPLAY_FORMAT = "d/m/Y"; 139 | 140 | render() { 141 | const { value, onChange, minDate, onClose, ...otherProps } = this.props; 142 | const options = { 143 | dateFormat: this.DISPLAY_FORMAT, 144 | allowInput: true, 145 | disableMobile: true, 146 | onClose: onClose, 147 | onOpen: this.onOpen 148 | }; 149 | if (minDate) { 150 | options.minDate = minDate; 151 | } 152 | return ( 153 | { 160 | // return 161 | // } 162 | // } 163 | {...otherProps} 164 | /> 165 | ); 166 | } 167 | } 168 | 169 | export default DatePicker; 170 | -------------------------------------------------------------------------------- /src/components/Input/DatePicker.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/common"; 2 | 3 | $bezier: cubic-bezier(0.23, 1, 0.32, 1); 4 | $slideTime: 400ms; 5 | 6 | $daySize: 39px; 7 | $padding: $daySize / 16; 8 | $dayMargin: 2px; 9 | $daysWidth: $daySize * 7 + $dayMargin * 14 + $padding * 2 + 2; 10 | $calendarWidth: $daysWidth; 11 | 12 | $monthNavHeight: 28px !default; 13 | $weekdaysHeight: 28px !default; 14 | $timeHeight: 40px; 15 | 16 | $disabledBorderColor: transparent; 17 | 18 | $selectedDayForeground: #fff; 19 | $selectedDayBackground: $colormain; 20 | 21 | input { 22 | &.custom-date-input { 23 | word-spacing:-3px 24 | } 25 | } 26 | 27 | @-webkit-keyframes fpFadeInDown { 28 | from { 29 | opacity: 0; 30 | transform: translate3d(0, -20px, 0); 31 | } 32 | 33 | to { 34 | opacity: 1; 35 | transform: translate3d(0, 0, 0); 36 | } 37 | } 38 | 39 | @-moz-keyframes fpFadeInDown { 40 | from { 41 | opacity: 0; 42 | transform: translate3d(0, -20px, 0); 43 | } 44 | 45 | to { 46 | opacity: 1; 47 | transform: translate3d(0, 0, 0); 48 | } 49 | } 50 | 51 | @-ms-keyframes fpFadeInDown { 52 | from { 53 | opacity: 0; 54 | transform: translate3d(0, -20px, 0); 55 | } 56 | 57 | to { 58 | opacity: 1; 59 | transform: translate3d(0, 0, 0); 60 | } 61 | } 62 | 63 | @-o-keyframes fpFadeInDown { 64 | from { 65 | opacity: 0; 66 | transform: translate3d(0, -20px, 0); 67 | } 68 | 69 | to { 70 | opacity: 1; 71 | transform: translate3d(0, 0, 0); 72 | } 73 | } 74 | 75 | @keyframes fpFadeInDown { 76 | from { 77 | opacity: 0; 78 | transform: translate3d(0, -20px, 0); 79 | } 80 | 81 | to { 82 | opacity: 1; 83 | transform: translate3d(0, 0, 0); 84 | } 85 | } 86 | 87 | .flatpickr-calendar { 88 | background: transparent; 89 | opacity: 0; 90 | display: none; 91 | text-align: center; 92 | visibility: hidden; 93 | padding: 0; 94 | animation: none; 95 | direction: ltr; 96 | border: 0; 97 | font-size: 14px; 98 | line-height: 24px; 99 | border-radius: 5px; 100 | position: absolute; 101 | width: $calendarWidth; 102 | box-sizing: border-box; 103 | touch-action: manipulation; 104 | 105 | &.open, &.inline { 106 | opacity: 1; 107 | max-height: 640px; 108 | visibility: visible; 109 | } 110 | 111 | &.open { 112 | display: inline-block; 113 | z-index: 99999; 114 | } 115 | 116 | &.animate.open { 117 | animation: fpFadeInDown 300ms $bezier; 118 | } 119 | 120 | &.inline { 121 | display: block; 122 | position: relative; 123 | top: 2px; 124 | } 125 | 126 | &.static { 127 | position: absolute; 128 | top: calc(100% + 2px); 129 | 130 | &.open { 131 | z-index: 999; 132 | display: block; 133 | } 134 | } 135 | 136 | &.multiMonth { 137 | .flatpickr-days .dayContainer:nth-child(n+1) { 138 | & .flatpickr-day.inRange:nth-child(7n+7) { 139 | box-shadow: none !important; 140 | } 141 | } 142 | 143 | .flatpickr-days .dayContainer:nth-child(n+2) { 144 | & .flatpickr-day.inRange:nth-child(7n+1) { 145 | box-shadow: -2px 0 0 #e6e6e6, 5px 0 0 #e6e6e6; 146 | } 147 | } 148 | } 149 | 150 | .hasWeeks, .hasTime { 151 | .dayContainer { 152 | border-bottom: 0; 153 | border-bottom-right-radius: 0; 154 | border-bottom-left-radius: 0; 155 | } 156 | } 157 | 158 | &.showTimeInput.hasTime { 159 | .flatpickr-time { 160 | height: $timeHeight; 161 | } 162 | } 163 | 164 | &.noCalendar.hasTime { 165 | .flatpickr-time { 166 | height: auto; 167 | } 168 | } 169 | 170 | &:before, &:after { 171 | position: absolute; 172 | display: block; 173 | pointer-events: none; 174 | border: solid transparent; 175 | content: ''; 176 | height: 0; 177 | width: 0; 178 | left: 22px; 179 | } 180 | 181 | &.rightMost { 182 | &:before, &:after { 183 | left: auto; 184 | right: 22px; 185 | } 186 | } 187 | 188 | &:before { 189 | border-width: 5px; 190 | margin: 0 -5px; 191 | } 192 | 193 | &:after { 194 | border-width: 4px; 195 | margin: 0 -4px; 196 | } 197 | 198 | &.arrowTop { 199 | &:before, &:after { 200 | bottom: 100%; 201 | } 202 | } 203 | 204 | &.arrowBottom { 205 | &:before, &:after { 206 | top: 100%; 207 | } 208 | } 209 | 210 | &:focus { 211 | outline: 0; 212 | } 213 | 214 | .flatpickr-months { 215 | display: flex; 216 | 217 | .flatpickr-month { 218 | height: $monthNavHeight; 219 | line-height: 1; 220 | text-align: center; 221 | position: relative; 222 | user-select: none; 223 | overflow: hidden; 224 | flex: 1; 225 | } 226 | 227 | .flatpickr-prev-month, .flatpickr-next-month { 228 | text-decoration: none; 229 | cursor: pointer; 230 | position: absolute; 231 | top: 0; 232 | line-height: 16px; 233 | height: $monthNavHeight; 234 | padding: 10px; 235 | z-index: 3; 236 | 237 | &.disabled { 238 | display: none; 239 | } 240 | 241 | i { 242 | position: relative; 243 | } 244 | 245 | &.flatpickr-prev-month { 246 | left: 0; 247 | } 248 | 249 | 250 | &.flatpickr-next-month { 251 | right: 0; 252 | } 253 | 254 | svg { 255 | width: 14px; 256 | height: 14px; 257 | 258 | path { 259 | transition: fill 0.1s; 260 | } 261 | } 262 | } 263 | } 264 | 265 | background: $date-picker-bg; 266 | box-shadow: 1px 0 0 $date-picker-border, -1px 0 0 $date-picker-border, 0 1px 0 $date-picker-border, 0 -1px 0 $date-picker-border, 0 3px 13px rgba(black, 0.08); 267 | 268 | &.arrowTop { 269 | &:before { 270 | border-bottom-color: $date-picker-border; 271 | } 272 | &:after { 273 | border-bottom-color: $date-picker-bg; 274 | } 275 | } 276 | 277 | &.arrowBottom { 278 | &:before { 279 | border-top-color: $date-picker-border; 280 | } 281 | &:after { 282 | border-top-color: $date-picker-bg; 283 | } 284 | } 285 | 286 | &.showTimeInput.hasTime { 287 | .flatpickr-time { 288 | border-top: 1px solid $date-picker-border; 289 | } 290 | } 291 | 292 | .flatpickr-months { 293 | .flatpickr-month { 294 | background: $date-picker-month-bg; 295 | color: $date-picker-month-fg; 296 | fill: $date-picker-month-fg; 297 | } 298 | 299 | .flatpickr-prev-month, .flatpickr-next-month { 300 | color: $date-picker-month-fg; 301 | fill: $date-picker-month-fg; 302 | 303 | &:hover { 304 | color: $date-picker-today-bg; 305 | svg { 306 | fill: $date-picker-arrow-hover; 307 | } 308 | } 309 | } 310 | } 311 | } 312 | 313 | .flatpickr-wrapper { 314 | position: relative; 315 | display: inline-block; 316 | } 317 | 318 | .numInputWrapper { 319 | position: relative; 320 | height: auto; 321 | 322 | input, span { 323 | display: inline-block; 324 | } 325 | 326 | input { 327 | width: 100%; 328 | &::-ms-clear { 329 | display: none; 330 | } 331 | 332 | &::-webkit-outer-spin-button, &::-webkit-inner-spin-button { 333 | margin: 0; 334 | -webkit-appearance: none; 335 | } 336 | } 337 | 338 | span { 339 | position: absolute; 340 | right: 0; 341 | width: 14px; 342 | padding: 0 4px 0 2px; 343 | height: 50%; 344 | line-height: 50%; 345 | opacity: 0; 346 | cursor: pointer; 347 | box-sizing: border-box; 348 | 349 | &:after { 350 | display: block; 351 | content: ""; 352 | position: absolute; 353 | } 354 | 355 | &.arrowUp { 356 | top: 0; 357 | border-bottom: 0; 358 | 359 | &:after { 360 | top: 26%; 361 | } 362 | } 363 | 364 | &.arrowDown { 365 | top: 50%; 366 | 367 | &:after { 368 | top: 40%; 369 | } 370 | } 371 | 372 | svg { 373 | width: inherit; 374 | height: auto; 375 | } 376 | } 377 | 378 | &:hover { 379 | span { 380 | opacity: 1; 381 | } 382 | } 383 | 384 | span { 385 | border: 1px solid rgba($date-picker-day-fg, 0.15); 386 | 387 | &:hover { 388 | background: rgba(invert($date-picker-bg), 0.1); 389 | } 390 | 391 | &:active { 392 | background: rgba(invert($date-picker-bg), 0.2); 393 | } 394 | 395 | &.arrowUp { 396 | &:after { 397 | border-left: 4px solid transparent; 398 | border-right: 4px solid transparent; 399 | border-bottom: 4px solid rgba($date-picker-day-fg, 0.6); 400 | } 401 | } 402 | 403 | &.arrowDown { 404 | &:after { 405 | border-left: 4px solid transparent; 406 | border-right: 4px solid transparent; 407 | border-top: 4px solid rgba($date-picker-day-fg, 0.6); 408 | } 409 | } 410 | 411 | svg { 412 | path { 413 | fill: rgba($date-picker-month-fg, 0.5); 414 | } 415 | } 416 | } 417 | 418 | &:hover { 419 | background: rgba(invert($date-picker-bg), 0.05); 420 | } 421 | } 422 | 423 | .flatpickr-current-month { 424 | font-size: 135%; 425 | font-weight: 300; 426 | color: inherit; 427 | position: absolute; 428 | width: 75%; 429 | left: 12.5%; 430 | padding: 0.22 * $monthNavHeight 0 0 0; 431 | line-height: 1; 432 | height: $monthNavHeight; 433 | display: inline-block; 434 | text-align: center; 435 | transform: translate3d(0px, 0px, 0px); 436 | 437 | span.cur-month { 438 | font-family: inherit; 439 | font-weight: 700; 440 | color: inherit; 441 | display: inline-block; 442 | margin-left: 0.5ch; 443 | padding: 0; 444 | } 445 | 446 | .numInputWrapper { 447 | width: 6ch; 448 | display: inline-block; 449 | } 450 | 451 | input.cur-year { 452 | background: transparent; 453 | box-sizing: border-box; 454 | color: inherit; 455 | cursor: text; 456 | padding: 0 0 0 0.5ch; 457 | margin: 0; 458 | display: inline-block; 459 | font-size: inherit; 460 | font-family: inherit; 461 | font-weight: 300; 462 | line-height: inherit; 463 | height: auto; 464 | border: 0; 465 | border-radius: 0; 466 | vertical-align: initial; 467 | -webkit-appearance: textfield; 468 | -moz-appearance: textfield; 469 | appearance: textfield; 470 | 471 | &:focus { 472 | outline: 0; 473 | } 474 | 475 | &[disabled], &[disabled]:hover { 476 | font-size: 100%; 477 | pointer-events: none; 478 | } 479 | } 480 | 481 | span.cur-month { 482 | &:hover { 483 | background: rgba(invert($date-picker-bg), 0.05); 484 | } 485 | } 486 | 487 | .numInputWrapper { 488 | span.arrowUp:after { 489 | border-bottom-color: $date-picker-month-fg; 490 | } 491 | 492 | span.arrowDown:after { 493 | border-top-color: $date-picker-month-fg; 494 | } 495 | } 496 | 497 | input.cur-year { 498 | &[disabled], &[disabled]:hover { 499 | color: rgba($date-picker-month-fg, 0.5); 500 | background: transparent; 501 | } 502 | } 503 | } 504 | 505 | .flatpickr-monthDropdown-months { 506 | border: none; 507 | border-radius: 0; 508 | box-sizing: border-box; 509 | color: inherit; 510 | cursor: pointer; 511 | font-size: inherit; 512 | font-family: inherit; 513 | font-weight: 300; 514 | height: $monthNavHeight - 6px; 515 | line-height: inherit; 516 | margin: -1px 0 0 0; 517 | outline: none; 518 | padding: 0 0 0 0.5ch; 519 | position: relative; 520 | vertical-align: initial; 521 | -webkit-box-sizing: border-box; 522 | -webkit-appearance: none; 523 | -moz-appearance: none; 524 | appearance: none; 525 | width: auto; 526 | 527 | &:focus, &:active { 528 | outline: none; 529 | } 530 | 531 | .flatpickr-monthDropdown-month { 532 | outline: none; 533 | padding: 0; 534 | } 535 | 536 | background: $date-picker-month-bg; 537 | 538 | &:hover { 539 | background: rgba(invert($date-picker-bg), 0.05); 540 | } 541 | 542 | .flatpickr-monthDropdown-month { 543 | background-color: $date-picker-month-bg; 544 | } 545 | } 546 | 547 | .flatpickr-weekdays { 548 | text-align: center; 549 | overflow: hidden; 550 | width: 100%; 551 | display: flex; 552 | align-items: center; 553 | height: $weekdaysHeight; 554 | 555 | .flatpickr-weekdaycontainer { 556 | display: flex; 557 | flex: 1; 558 | } 559 | 560 | background: $date-picker-weekdays-bg; 561 | } 562 | 563 | span.flatpickr-weekday { 564 | cursor: default; 565 | font-size: 90%; 566 | line-height: 1; 567 | margin: 0; 568 | text-align: center; 569 | display: block; 570 | flex: 1; 571 | font-weight: bolder; 572 | 573 | background: $date-picker-month-bg; 574 | color: $date-picker-weekdays-fg; 575 | } 576 | 577 | .dayContainer, .flatpickr-weeks { 578 | padding: 1px 0 0 0; 579 | } 580 | 581 | .flatpickr-days { 582 | position: relative; 583 | overflow: hidden; 584 | display: flex; 585 | align-items: flex-start; 586 | width: $daysWidth; 587 | 588 | &:focus { 589 | outline: 0; 590 | } 591 | } 592 | 593 | .dayContainer { 594 | padding: 0; 595 | outline: 0; 596 | text-align: left; 597 | width: $daysWidth; 598 | min-width: $daysWidth; 599 | max-width: $daysWidth; 600 | box-sizing: border-box; 601 | display: inline-block; 602 | display: -ms-flexbox; 603 | display: flex; 604 | flex-wrap: wrap; 605 | -ms-flex-wrap: wrap; 606 | -ms-flex-pack: justify; 607 | justify-content: space-around; 608 | transform: translate3d(0px, 0px, 0px); 609 | opacity: 1; 610 | 611 | & + .dayContainer { 612 | box-shadow: -1px 0 0 $date-picker-border; 613 | } 614 | } 615 | 616 | .flatpickr-day { 617 | background: none; 618 | border: 1px solid transparent; 619 | border-radius: 150px; 620 | box-sizing: border-box; 621 | cursor: pointer; 622 | 623 | font-weight: 400; 624 | width: 14.2857143%; 625 | flex-basis: 14.2857143%; 626 | max-width: $daySize; 627 | height: $daySize; 628 | line-height: $daySize; 629 | margin: 0; 630 | 631 | display: inline-block; 632 | position: relative; 633 | justify-content: center; 634 | text-align: center; 635 | 636 | &, &.prevMonthDay, &.nextMonthDay { 637 | &.inRange, &.today.inRange, &:hover, &:focus { 638 | cursor: pointer; 639 | outline: 0; 640 | } 641 | } 642 | 643 | &.selected, &.startRange, &.endRange { 644 | &.startRange { 645 | border-radius: 50px 0 0 50px; 646 | } 647 | 648 | &.endRange { 649 | border-radius: 0 50px 50px 0; 650 | } 651 | 652 | &.startRange.endRange { 653 | border-radius: 50px; 654 | } 655 | } 656 | 657 | &.inRange { 658 | border-radius: 0; 659 | } 660 | 661 | &.flatpickr-disabled, &.flatpickr-disabled:hover, 662 | &.prevMonthDay, &.nextMonthDay, 663 | &.notAllowed, &.notAllowed.prevMonthDay, &.notAllowed.nextMonthDay { 664 | cursor: default; 665 | } 666 | 667 | &.flatpickr-disabled, &.flatpickr-disabled:hover { 668 | cursor: not-allowed; 669 | } 670 | 671 | &.week.selected { 672 | border-radius: 0; 673 | } 674 | 675 | &.hidden { 676 | visibility: hidden; 677 | } 678 | 679 | background: none; 680 | border: 1px solid transparent; 681 | color: $date-picker-day-fg; 682 | 683 | &, &.prevMonthDay, &.nextMonthDay { 684 | &.inRange, &.today.inRange, &:hover, &:focus { 685 | background: $date-picker-day-hover-bg; 686 | border-color: $date-picker-day-hover-bg; 687 | } 688 | } 689 | 690 | &.today { 691 | border-color: $date-picker-today-border; 692 | 693 | &:hover, &:focus { 694 | border-color: $date-picker-today-border; 695 | background: $date-picker-today-bg; 696 | color: $date-picker-today-fg; 697 | } 698 | } 699 | 700 | &.selected, &.startRange, &.endRange { 701 | &, &.inRange, &:focus, &:hover, &.prevMonthDay, &.nextMonthDay { 702 | background: $selectedDayBackground; 703 | box-shadow: none; 704 | color: $selectedDayForeground; 705 | border-color: $selectedDayBackground; 706 | } 707 | 708 | &.startRange + .endRange:not(:nth-child(7n+1)) { 709 | box-shadow: -5 * $dayMargin 0 0 $selectedDayBackground; 710 | } 711 | } 712 | 713 | &.inRange { 714 | border-radius: 0; 715 | box-shadow: -2.5 * $dayMargin 0 0 $date-picker-day-hover-bg, 2.5 * $dayMargin 0 0 $date-picker-day-hover-bg; 716 | } 717 | 718 | &.flatpickr-disabled, &.flatpickr-disabled:hover, 719 | &.prevMonthDay, &.nextMonthDay, 720 | &.notAllowed, &.notAllowed.prevMonthDay, &.notAllowed.nextMonthDay { 721 | color: rgba($date-picker-day-fg, 0.3); 722 | background: transparent; 723 | border-color: $disabledBorderColor; 724 | } 725 | 726 | &.flatpickr-disabled, &.flatpickr-disabled:hover { 727 | color: rgba($date-picker-day-fg, 0.1); 728 | } 729 | } 730 | 731 | .rangeMode .flatpickr-day { 732 | margin-top: 1px; 733 | } 734 | 735 | .flatpickr-weekwrapper { 736 | float: left; 737 | 738 | .flatpickr-weeks { 739 | padding: 0 12px; 740 | } 741 | 742 | .flatpickr-weekday { 743 | float: none; 744 | width: 100%; 745 | line-height: $weekdaysHeight; 746 | } 747 | 748 | span.flatpickr-day { 749 | &, &:hover { 750 | display: block; 751 | width: 100%; 752 | max-width: none; 753 | cursor: default; 754 | } 755 | } 756 | 757 | .flatpickr-weeks { 758 | box-shadow: 1px 0 0 $date-picker-border; 759 | } 760 | 761 | span.flatpickr-day { 762 | &, &:hover { 763 | color: rgba($date-picker-day-fg, 0.3); 764 | background: transparent; 765 | border: none; 766 | } 767 | } 768 | } 769 | 770 | .flatpickr-innerContainer { 771 | display: flex; 772 | box-sizing: border-box; 773 | overflow: hidden; 774 | } 775 | 776 | .flatpickr-rContainer { 777 | display: inline-block; 778 | padding: 0; 779 | box-sizing: border-box; 780 | } 781 | 782 | .flatpickr-time { 783 | text-align: center; 784 | outline: 0; 785 | height: 0; 786 | line-height: $timeHeight; 787 | max-height: $timeHeight; 788 | box-sizing: border-box; 789 | overflow: hidden; 790 | display: flex; 791 | 792 | &:after { 793 | content: ""; 794 | display: table; 795 | clear: both; 796 | } 797 | 798 | .numInputWrapper { 799 | flex: 1; 800 | width: 40%; 801 | height: $timeHeight; 802 | float: left; 803 | } 804 | 805 | &.hasSeconds .numInputWrapper { 806 | width: 26%; 807 | } 808 | 809 | &.time24hr .numInputWrapper { 810 | width: 49%; 811 | } 812 | 813 | input { 814 | background: transparent; 815 | box-shadow: none; 816 | border: 0; 817 | border-radius: 0; 818 | text-align: center; 819 | margin: 0; 820 | padding: 0; 821 | height: inherit; 822 | line-height: inherit; 823 | font-size: 14px; 824 | position: relative; 825 | box-sizing: border-box; 826 | -webkit-appearance: textfield; 827 | -moz-appearance: textfield; 828 | appearance: textfield; 829 | 830 | &.flatpickr-hour { 831 | font-weight: bold; 832 | } 833 | 834 | &.flatpickr-minute, &.flatpickr-second { 835 | font-weight: 400; 836 | } 837 | 838 | &:focus { 839 | outline: 0; 840 | border: 0; 841 | } 842 | } 843 | 844 | .flatpickr-time-separator, .flatpickr-am-pm { 845 | height: inherit; 846 | float: left; 847 | line-height: inherit; 848 | font-weight: bold; 849 | width: 2%; 850 | user-select: none; 851 | align-self: center; 852 | } 853 | 854 | .flatpickr-am-pm { 855 | outline: 0; 856 | width: 18%; 857 | cursor: pointer; 858 | text-align: center; 859 | font-weight: 400; 860 | } 861 | 862 | .numInputWrapper { 863 | span.arrowUp:after { 864 | border-bottom-color: $date-picker-day-fg; 865 | } 866 | 867 | span.arrowDown:after { 868 | border-top-color: $date-picker-day-fg; 869 | } 870 | } 871 | 872 | input { 873 | color: $date-picker-day-fg; 874 | } 875 | 876 | .flatpickr-time-separator, .flatpickr-am-pm { 877 | color: $date-picker-day-fg; 878 | } 879 | 880 | input, .flatpickr-am-pm { 881 | &:hover, &:focus { 882 | background: lighten($date-picker-day-hover-bg, 3); 883 | } 884 | } 885 | } 886 | 887 | .flatpickr-input[readonly] { 888 | cursor: pointer; 889 | } 890 | -------------------------------------------------------------------------------- /src/components/Input/InputSuggest.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Autosuggest from 'react-autosuggest'; 3 | import { connect } from 'react-redux'; 4 | import _ from 'lodash'; 5 | 6 | import "./InputSuggest.scss"; 7 | 8 | const isAlphaNumericChar = keycode => { 9 | return (keycode >= 48 && keycode <= 57) || (keycode >= 65 && keycode <= 90); 10 | }; 11 | 12 | class InputSuggestion extends React.Component { 13 | constructor() { 14 | super(); 15 | 16 | this.state = { 17 | textInput: '', 18 | suggestions: [] 19 | }; 20 | document.addEventListener('keydown', this.freeTyping.bind(this), false); 21 | } 22 | 23 | freeTyping(e) { 24 | if (!isAlphaNumericChar(e.keyCode)) { 25 | return; 26 | } 27 | if (e.target.value === undefined) { 28 | this.inputSearch.focus(); 29 | } 30 | } 31 | 32 | getSuggestions = textInput => { 33 | // if (!textInput || textInput.length === 0) return []; //chưa nhập -> chưa gợi ý 34 | const inputValue = textInput.trim().toLowerCase(); 35 | 36 | if (!this.props.inputsWithIndex) { 37 | return [{ textInput: textInput }]; 38 | }; 39 | let inputsWithIndex = this.props.inputsWithIndex; 40 | let keyArr = Object.keys(inputsWithIndex).filter( 41 | textInput => { 42 | return textInput.toLowerCase().indexOf(inputValue) >= 0 43 | } 44 | ); 45 | var suggestArr = keyArr.map(function (key) { 46 | return inputsWithIndex[key]; 47 | }); 48 | 49 | return suggestArr; 50 | }; 51 | 52 | storeInputReference = autosuggest => { 53 | if (autosuggest !== null) { 54 | this.inputSearch = autosuggest.input; 55 | } 56 | }; 57 | 58 | shouldRenderSuggestions = value => { 59 | return true; 60 | }; 61 | 62 | getSuggestionValue = suggestion => { 63 | this.props.onSelected(suggestion); 64 | return suggestion.displayName; 65 | } 66 | 67 | renderSuggestion = suggestion => { 68 | return ( 69 |
70 | {suggestion.displayName} 71 |
72 | ); 73 | }; 74 | 75 | onSuggestionsFetchRequested = ({ value }) => { 76 | this.setState({ 77 | suggestions: this.sortSuggestions(this.getSuggestions(value), value) 78 | }); 79 | }; 80 | 81 | sortSuggestions(suggestions, value) { 82 | var results = _.sortBy(suggestions, (element) => { 83 | return element.displayName 84 | }) 85 | return results; 86 | } 87 | 88 | onSuggestionsClearRequested = () => { 89 | this.setState({ 90 | suggestions: [] 91 | }); 92 | }; 93 | 94 | onSuggestionSelected = (event, selected) => { 95 | this.props.onSelected(selected.suggestion) 96 | this.setState({ 97 | textInput: selected.suggestion && selected.suggestion.displayName 98 | }); 99 | }; 100 | 101 | handleChangeInput = (event, { newValue }) => { 102 | this.setState({ 103 | textInput: newValue || '' 104 | }); 105 | }; 106 | 107 | reset() { 108 | this.setState({ 109 | textInput: '' 110 | }); 111 | this.onSuggestionsFetchRequested({ value: '' }); 112 | } 113 | 114 | handleBlurInput() { 115 | // this.setState({ 116 | // textInput: '' 117 | // }); 118 | } 119 | 120 | render() { 121 | const { textInput, suggestions } = this.state; 122 | const inputProps = { 123 | value: textInput, 124 | className: "custom-form-control", 125 | onChange: this.handleChangeInput, 126 | onClick: () => { 127 | this.reset(); 128 | }, 129 | onBlur: () => { 130 | this.handleBlurInput(); 131 | } 132 | }; 133 | return ( 134 | 146 | ); 147 | } 148 | } 149 | 150 | const mapStateToProps = state => { 151 | return { 152 | }; 153 | }; 154 | 155 | export default connect(mapStateToProps, null)(InputSuggestion); 156 | -------------------------------------------------------------------------------- /src/components/Input/InputSuggest.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/common"; 2 | 3 | .react-autosuggest__suggestions-container { 4 | min-width: 90%; 5 | position: absolute; 6 | top: 50px; 7 | z-index: 4; 8 | max-height: 140px; 9 | overflow: auto; 10 | background: $footer-container; 11 | box-shadow: 2px 2px 4px 0 $box-shadow-color; 12 | -webkit-box-shadow: 2px 2px 4px 0 $box-shadow-color; 13 | } 14 | 15 | .react-autosuggest__suggestions-list { 16 | list-style-type: none; 17 | margin-bottom: 0; 18 | padding: 0px; 19 | } 20 | 21 | .react-autosuggest__suggestion { 22 | padding: 0 5px; 23 | height: 28px; 24 | line-height: 28px; 25 | border-bottom: 1px solid $border; 26 | &:hover { 27 | cursor: pointer; 28 | background-color: darken($footer-container, 5); 29 | } 30 | } 31 | 32 | .react-autosuggest__suggestion--highlighted { 33 | background-color: darken($footer-container, 5); 34 | } -------------------------------------------------------------------------------- /src/components/Input/index.js: -------------------------------------------------------------------------------- 1 | export { default as DatePicker } from './DatePicker'; 2 | export { default as InputSuggest } from './InputSuggest'; -------------------------------------------------------------------------------- /src/components/Navigator.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { Link, withRouter } from 'react-router-dom'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { connect } from 'react-redux'; 5 | 6 | import './Navigator.scss'; 7 | 8 | class MenuGroup extends Component { 9 | 10 | render() { 11 | const { name, children } = this.props; 12 | return ( 13 |
  • 14 |
    15 | 16 |
    17 |
      18 | {children} 19 |
    20 |
  • 21 | ); 22 | } 23 | } 24 | 25 | class Menu extends Component { 26 | 27 | render() { 28 | const { name, active, link, children, onClick, hasSubMenu, onLinkClick } = this.props; 29 | return ( 30 |
  • 31 | {hasSubMenu ? ( 32 | 33 | 39 | 40 |
    41 | 42 |
    43 |
    44 |
    45 |
      46 | {children} 47 |
    48 |
    49 |
    50 | ) : ( 51 | 52 | 53 | 54 | )} 55 |
  • 56 | ); 57 | } 58 | } 59 | 60 | class SubMenu extends Component { 61 | 62 | getItemClass = path => { 63 | return this.props.location.pathname === path ? "active" : ""; 64 | }; 65 | 66 | render() { 67 | const { name, link, onLinkClick } = this.props; 68 | return ( 69 |
  • 70 | 71 | 72 | 73 |
  • 74 | ); 75 | } 76 | } 77 | 78 | const MenuGroupWithRouter = withRouter(MenuGroup); 79 | const MenuWithRouter = withRouter(Menu); 80 | const SubMenuWithRouter = withRouter(SubMenu); 81 | 82 | const withRouterInnerRef = (WrappedComponent) => { 83 | 84 | class InnerComponentWithRef extends React.Component { 85 | render() { 86 | const { forwardRef, ...rest } = this.props; 87 | return ; 88 | } 89 | } 90 | 91 | const ComponentWithRef = withRouter(InnerComponentWithRef, { withRef: true }); 92 | 93 | return React.forwardRef((props, ref) => { 94 | return ; 95 | }); 96 | }; 97 | 98 | class Navigator extends Component { 99 | state = { 100 | expandedMenu: {} 101 | }; 102 | 103 | toggle = (groupIndex, menuIndex) => { 104 | const expandedMenu = {}; 105 | const needExpand = !(this.state.expandedMenu[groupIndex + '_' + menuIndex] === true); 106 | if (needExpand) { 107 | expandedMenu[groupIndex + '_' + menuIndex] = true; 108 | } 109 | 110 | this.setState({ 111 | expandedMenu: expandedMenu 112 | }); 113 | }; 114 | 115 | isMenuHasSubMenuActive = (location, subMenus, link) => { 116 | if (subMenus) { 117 | if (subMenus.length === 0) { 118 | return false; 119 | } 120 | 121 | const currentPath = location.pathname; 122 | for (let i = 0; i < subMenus.length; i++) { 123 | const subMenu = subMenus[i]; 124 | if (subMenu.link === currentPath) { 125 | return true; 126 | } 127 | } 128 | } 129 | 130 | if (link) { 131 | return this.props.location.pathname === link; 132 | } 133 | 134 | return false; 135 | }; 136 | 137 | checkActiveMenu = () => { 138 | const { menus, location } = this.props; 139 | outerLoop: 140 | for (let i = 0; i < menus.length; i++) { 141 | const group = menus[i]; 142 | if (group.menus && group.menus.length > 0) { 143 | for (let j = 0; j < group.menus.length; j++) { 144 | const menu = group.menus[j]; 145 | if (menu.subMenus && menu.subMenus.length > 0) { 146 | if (this.isMenuHasSubMenuActive(location, menu.subMenus, null)) { 147 | const key = i + '_' + j; 148 | this.setState({ 149 | expandedMenu: { 150 | [key]: true 151 | } 152 | }); 153 | break outerLoop; 154 | } 155 | } 156 | } 157 | } 158 | } 159 | }; 160 | 161 | componentDidMount() { 162 | this.checkActiveMenu(); 163 | }; 164 | 165 | // componentWillReceiveProps(nextProps, prevState) { 166 | // const { location, setAccountMenuPath, setSettingMenuPath } = this.props; 167 | // const { location: nextLocation } = nextProps; 168 | // if (location !== nextLocation) { 169 | // let pathname = nextLocation && nextLocation.pathname; 170 | // if ((pathname.startsWith('/account/') || pathname.startsWith('/fds/account/'))) { 171 | // setAccountMenuPath(pathname); 172 | // } 173 | // if (pathname.startsWith('/settings/')) { 174 | // setSettingMenuPath(pathname); 175 | // }; 176 | // }; 177 | // }; 178 | 179 | componentDidUpdate(prevProps, prevState) { 180 | const { location } = this.props; 181 | const { location: prevLocation } = prevProps; 182 | if (location !== prevLocation) { 183 | this.checkActiveMenu(); 184 | }; 185 | }; 186 | 187 | render() { 188 | const { menus, location, onLinkClick } = this.props; 189 | return ( 190 | 191 |
      192 | { 193 | menus.map((group, groupIndex) => { 194 | return ( 195 | 196 | 197 | {group.menus ? ( 198 | group.menus.map((menu, menuIndex) => { 199 | const isMenuHasSubMenuActive = this.isMenuHasSubMenuActive(location, menu.subMenus, menu.link); 200 | const isSubMenuOpen = this.state.expandedMenu[groupIndex + '_' + menuIndex] === true; 201 | return ( 202 | this.toggle(groupIndex, menuIndex)} 210 | onLinkClick={onLinkClick} 211 | > 212 | {menu.subMenus && menu.subMenus.map((subMenu, subMenuIndex) => ( 213 | 220 | ))} 221 | 222 | ); 223 | }) 224 | ) : null} 225 | 226 | 227 | ); 228 | }) 229 | } 230 |
    231 |
    232 | ); 233 | } 234 | } 235 | 236 | const mapStateToProps = state => { 237 | return { 238 | }; 239 | }; 240 | 241 | const mapDispatchToProps = dispatch => { 242 | return { 243 | } 244 | } 245 | 246 | export default withRouterInnerRef(connect(mapStateToProps, mapDispatchToProps)(Navigator)); 247 | -------------------------------------------------------------------------------- /src/components/Navigator.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/common.scss"; 2 | 3 | .navigator-menu { 4 | display: flex; 5 | margin: 0; 6 | padding: 0; 7 | &.list-unstyled, 8 | .list-unstyled { 9 | list-style-type: none; 10 | } 11 | .menu-group { 12 | &:hover { 13 | cursor: pointer; 14 | background-color: darken($colormain, 5); 15 | } 16 | .menu-group-name { 17 | line-height: 40px; 18 | padding: 0 15px; 19 | border-right: 1px solid $border; 20 | &:first-child { 21 | border-left: 1px solid $border; 22 | } 23 | } 24 | .menu-list { 25 | display: none; 26 | background-color: $bg-menu-color; 27 | box-shadow: 2px 2px 4px 0 $box-shadow-color; 28 | -webkit-box-shadow: 2px 2px 4px 0 $box-shadow-color; 29 | color: $text-in-light; 30 | position: absolute; 31 | padding: 0; 32 | .menu { 33 | width: 205px; 34 | padding: 0 15px; 35 | height: 35px; 36 | line-height: 35px; 37 | text-transform: none; 38 | .menu-link { 39 | text-decoration: none; 40 | color: $text-in-light; 41 | } 42 | .sub-menu-list { 43 | display: none; 44 | background-color: $bg-menu-color; 45 | box-shadow: 2px 2px 4px 0 $box-shadow-color; 46 | -webkit-box-shadow: 2px 2px 4px 0 $box-shadow-color; 47 | position: absolute; 48 | top: 0; 49 | left: 205px; 50 | padding: 0; 51 | .sub-menu { 52 | padding: 0 15px; 53 | height: 35px; 54 | line-height: 35px; 55 | white-space: nowrap; 56 | &:hover { 57 | background-color: darken($bg-menu-color, 5); 58 | } 59 | .sub-menu-link { 60 | text-decoration: none; 61 | color: $text-in-light; 62 | } 63 | a { 64 | display: block; 65 | } 66 | &.active a { 67 | font-weight: 500; 68 | color: $colormain; 69 | } 70 | } 71 | } 72 | &.active span { 73 | font-weight: 500; 74 | color: $colormain; 75 | } 76 | &:hover { 77 | background-color: darken($bg-menu-color, 3); 78 | .sub-menu-list { 79 | display: block; 80 | } 81 | } 82 | .icon-right { 83 | display: block; 84 | position: absolute; 85 | top: 0; 86 | right: 10px; 87 | } 88 | } 89 | } 90 | &:hover { 91 | .menu-list { 92 | display: block; 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | api: { 3 | API_BASE_URL: "http://localhost:8080/", 4 | ROUTER_BASE_NAME: null, 5 | }, 6 | app: { 7 | /** 8 | * The base URL for all locations. If your app is served from a sub-directory on your server, you'll want to set 9 | * this to the sub-directory. A properly formatted basename should have a leading slash, but no trailing slash. 10 | */ 11 | ROUTER_BASE_NAME: null, 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Switch } from 'react-router-dom'; 4 | import { ConnectedRouter as Router } from 'connected-react-router'; 5 | import { history } from '../redux' 6 | import { ToastContainer } from 'react-toastify'; 7 | 8 | 9 | import { userIsAuthenticated, userIsNotAuthenticated } from '../hoc/authentication'; 10 | 11 | import { path } from '../utils' 12 | 13 | import Home from '../routes/Home'; 14 | import Login from '../routes/Login'; 15 | import Header from './Header/Header'; 16 | import System from '../routes/System'; 17 | 18 | import { CustomToastCloseButton } from '../components/CustomToast'; 19 | import ConfirmModal from '../components/ConfirmModal'; 20 | 21 | class App extends Component { 22 | 23 | handlePersistorState = () => { 24 | const { persistor } = this.props; 25 | let { bootstrapped } = persistor.getState(); 26 | if (bootstrapped) { 27 | if (this.props.onBeforeLift) { 28 | Promise.resolve(this.props.onBeforeLift()) 29 | .then(() => this.setState({ bootstrapped: true })) 30 | .catch(() => this.setState({ bootstrapped: true })); 31 | } else { 32 | this.setState({ bootstrapped: true }); 33 | } 34 | } 35 | }; 36 | 37 | componentDidMount() { 38 | this.handlePersistorState(); 39 | } 40 | 41 | render() { 42 | return ( 43 | 44 | 45 |
    46 | 47 | {this.props.isLoggedIn &&
    } 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | } 62 | /> 63 |
    64 |
    65 |
    66 | ) 67 | } 68 | } 69 | 70 | const mapStateToProps = state => { 71 | return { 72 | started: state.app.started, 73 | isLoggedIn: state.admin.isLoggedIn 74 | }; 75 | }; 76 | 77 | const mapDispatchToProps = dispatch => { 78 | return { 79 | }; 80 | }; 81 | 82 | export default connect(mapStateToProps, mapDispatchToProps)(App); -------------------------------------------------------------------------------- /src/containers/App.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/common.scss"; -------------------------------------------------------------------------------- /src/containers/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import * as actions from "../../store/actions"; 5 | import Navigator from '../../components/Navigator'; 6 | import { adminMenu } from './menuApp'; 7 | import './Header.scss'; 8 | 9 | class Header extends Component { 10 | 11 | render() { 12 | const { processLogout } = this.props; 13 | 14 | return ( 15 |
    16 | {/* thanh navigator */} 17 |
    18 | 19 |
    20 | 21 | {/* nút logout */} 22 |
    23 | 24 |
    25 |
    26 | ); 27 | } 28 | 29 | } 30 | 31 | const mapStateToProps = state => { 32 | return { 33 | isLoggedIn: state.admin.isLoggedIn 34 | }; 35 | }; 36 | 37 | const mapDispatchToProps = dispatch => { 38 | return { 39 | processLogout: () => dispatch(actions.processLogout()), 40 | }; 41 | }; 42 | 43 | export default connect(mapStateToProps, mapDispatchToProps)(Header); 44 | -------------------------------------------------------------------------------- /src/containers/Header/Header.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/common.scss"; 2 | 3 | .header-container { 4 | z-index: 99; 5 | display: flex; 6 | justify-content: space-between; 7 | background-color: $colormain; 8 | color: $common-text-color; 9 | height: 40px; 10 | position: relative; 11 | .btn-logout { 12 | color: $common-text-color; 13 | line-height: 40px; 14 | height: 40px; 15 | padding: 0 10px; 16 | &:hover { 17 | background-color: darken($colormain, 5); 18 | } 19 | i { 20 | font-size: $base-size + 1px; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/containers/Header/menuApp.js: -------------------------------------------------------------------------------- 1 | export const adminMenu = [ 2 | { //hệ thống 3 | name: 'menu.system.header', menus: [ 4 | { 5 | name: 'menu.system.system-administrator.header', 6 | subMenus: [ 7 | { name: 'menu.system.system-administrator.user-manage', link: '/system/user-manage' }, 8 | { name: 'menu.system.system-administrator.product-manage', link: '/system/product-manage' }, 9 | { name: 'menu.system.system-administrator.register-package-group-or-account', link: '/system/register-package-group-or-account' }, 10 | ] 11 | }, 12 | // { name: 'menu.system.system-parameter.header', link: '/system/system-parameter' }, 13 | ] 14 | }, 15 | ]; -------------------------------------------------------------------------------- /src/containers/System/ProductManage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { connect } from 'react-redux'; 4 | class ProductManage extends Component { 5 | 6 | state = { 7 | 8 | } 9 | 10 | componentDidMount() { 11 | } 12 | 13 | 14 | render() { 15 | return ( 16 |
    Manage products
    17 | ) 18 | } 19 | 20 | } 21 | 22 | const mapStateToProps = state => { 23 | return { 24 | }; 25 | }; 26 | 27 | const mapDispatchToProps = dispatch => { 28 | return { 29 | }; 30 | }; 31 | 32 | export default connect(mapStateToProps, mapDispatchToProps)(ProductManage); 33 | -------------------------------------------------------------------------------- /src/containers/System/RegisterPackageGroupOrAcc.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { connect } from 'react-redux'; 4 | class RegisterPackageGroupOrAcc extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | } 10 | 11 | 12 | 13 | render() { 14 | return ( 15 |
    16 | register package group or account 17 |
    ) 18 | } 19 | 20 | } 21 | 22 | const mapStateToProps = state => { 23 | return { 24 | 25 | }; 26 | }; 27 | 28 | const mapDispatchToProps = dispatch => { 29 | return { 30 | }; 31 | }; 32 | 33 | export default connect(mapStateToProps, mapDispatchToProps)(RegisterPackageGroupOrAcc); 34 | -------------------------------------------------------------------------------- /src/containers/System/UserManage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { connect } from 'react-redux'; 4 | class UserManage extends Component { 5 | 6 | state = { 7 | 8 | } 9 | 10 | componentDidMount() { 11 | 12 | } 13 | 14 | 15 | render() { 16 | return ( 17 |
    Manage users
    18 | ); 19 | } 20 | 21 | } 22 | 23 | const mapStateToProps = state => { 24 | return { 25 | }; 26 | }; 27 | 28 | const mapDispatchToProps = dispatch => { 29 | return { 30 | }; 31 | }; 32 | 33 | export default connect(mapStateToProps, mapDispatchToProps)(UserManage); 34 | -------------------------------------------------------------------------------- /src/hoc/IntlProviderWrapper.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { connect } from 'react-redux'; 3 | import { IntlProvider } from "react-intl"; 4 | 5 | import '@formatjs/intl-pluralrules/polyfill'; 6 | import '@formatjs/intl-pluralrules/locale-data/en'; 7 | import '@formatjs/intl-pluralrules/locale-data/vi'; 8 | 9 | import '@formatjs/intl-relativetimeformat/polyfill'; 10 | import '@formatjs/intl-relativetimeformat/locale-data/en'; 11 | import '@formatjs/intl-relativetimeformat/locale-data/vi'; 12 | 13 | import { LanguageUtils } from '../utils' 14 | 15 | const messages = LanguageUtils.getFlattenedMessages(); 16 | 17 | class IntlProviderWrapper extends Component { 18 | 19 | render() { 20 | const { children, language } = this.props; 21 | return ( 22 | 26 | {children} 27 | 28 | ); 29 | } 30 | } 31 | 32 | const mapStateToProps = state => { 33 | return { 34 | language: state.app.language 35 | }; 36 | }; 37 | 38 | export default connect(mapStateToProps, null)(IntlProviderWrapper); 39 | -------------------------------------------------------------------------------- /src/hoc/authentication.js: -------------------------------------------------------------------------------- 1 | import locationHelperBuilder from "redux-auth-wrapper/history4/locationHelper"; 2 | import { connectedRouterRedirect } from "redux-auth-wrapper/history4/redirect"; 3 | 4 | const locationHelper = locationHelperBuilder({}); 5 | 6 | export const userIsAuthenticated = connectedRouterRedirect({ 7 | authenticatedSelector: state => state.admin.isLoggedIn, 8 | wrapperDisplayName: 'UserIsAuthenticated', 9 | redirectPath: '/login' 10 | }); 11 | 12 | export const userIsNotAuthenticated = connectedRouterRedirect({ 13 | // Want to redirect the user when they are authenticated 14 | authenticatedSelector: state => !state.admin.isLoggedIn, 15 | wrapperDisplayName: 'UserIsNotAuthenticated', 16 | redirectPath: (state, ownProps) => locationHelper.getRedirectQueryParam(ownProps) || '/', 17 | allowRedirectBack: false 18 | }); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import 'react-toastify/dist/ReactToastify.css'; 4 | import './styles/styles.scss'; 5 | 6 | import App from './containers/App'; 7 | import * as serviceWorker from './serviceWorker'; 8 | import IntlProviderWrapper from "./hoc/IntlProviderWrapper"; 9 | 10 | 11 | import { Provider } from 'react-redux'; 12 | import reduxStore, { persistor } from './redux'; 13 | 14 | const renderApp = () => { 15 | ReactDOM.render( 16 | 17 | 18 | 19 | 20 | , 21 | document.getElementById('root') 22 | ); 23 | }; 24 | 25 | renderApp(); 26 | // If you want your app to work offline and load faster, you can change 27 | // unregister() to register() below. Note this comes with some pitfalls. 28 | // Learn more about service workers: https://bit.ly/CRA-PWA 29 | serviceWorker.unregister(); 30 | -------------------------------------------------------------------------------- /src/redux.js: -------------------------------------------------------------------------------- 1 | import { logger } from "redux-logger"; 2 | import thunkMiddleware from "redux-thunk"; 3 | import { routerMiddleware } from 'connected-react-router'; 4 | import { createBrowserHistory } from 'history'; 5 | 6 | import { createStore, applyMiddleware, compose } from 'redux'; 7 | import { createStateSyncMiddleware } from 'redux-state-sync'; 8 | import { persistStore } from 'redux-persist'; 9 | 10 | import createRootReducer from './store/reducers/rootReducer'; 11 | import actionTypes from './store/actions/actionTypes'; 12 | 13 | const environment = process.env.NODE_ENV || "development"; 14 | let isDevelopment = environment === "development"; 15 | 16 | //hide redux logs 17 | isDevelopment = false; 18 | 19 | 20 | export const history = createBrowserHistory({ basename: process.env.REACT_APP_ROUTER_BASE_NAME }); 21 | 22 | const reduxStateSyncConfig = { 23 | whitelist: [ 24 | actionTypes.APP_START_UP_COMPLETE, 25 | ] 26 | } 27 | 28 | const rootReducer = createRootReducer(history); 29 | const middleware = [ 30 | routerMiddleware(history), 31 | thunkMiddleware, 32 | createStateSyncMiddleware(reduxStateSyncConfig), 33 | ] 34 | if (isDevelopment) middleware.push(logger); 35 | 36 | const composeEnhancers = (isDevelopment && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose; 37 | 38 | const reduxStore = createStore( 39 | rootReducer, 40 | composeEnhancers(applyMiddleware(...middleware)), 41 | ) 42 | 43 | export const dispatch = reduxStore.dispatch; 44 | 45 | export const persistor = persistStore(reduxStore); 46 | 47 | export default reduxStore; -------------------------------------------------------------------------------- /src/routes/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | 5 | class Home extends Component { 6 | 7 | render() { 8 | const { isLoggedIn } = this.props; 9 | let linkToRedirect = isLoggedIn ? '/system/user-manage' : '/login'; 10 | 11 | return ( 12 | 13 | ); 14 | } 15 | 16 | } 17 | 18 | const mapStateToProps = state => { 19 | return { 20 | isLoggedIn: state.admin.isLoggedIn 21 | }; 22 | }; 23 | 24 | const mapDispatchToProps = dispatch => { 25 | return { 26 | }; 27 | }; 28 | 29 | export default connect(mapStateToProps, mapDispatchToProps)(Home); 30 | -------------------------------------------------------------------------------- /src/routes/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { push } from "connected-react-router"; 4 | 5 | import * as actions from "../store/actions"; 6 | import { KeyCodeUtils, LanguageUtils } from "../utils"; 7 | 8 | import userIcon from '../../src/assets/images/user.svg'; 9 | import passIcon from '../../src/assets/images/pass.svg'; 10 | import './Login.scss'; 11 | import { FormattedMessage } from 'react-intl'; 12 | 13 | import adminService from '../services/adminService'; 14 | 15 | class Login extends Component { 16 | constructor(props) { 17 | super(props); 18 | this.btnLogin = React.createRef(); 19 | } 20 | 21 | initialState = { 22 | username: '', 23 | password: '', 24 | loginError: '' 25 | } 26 | 27 | state = { 28 | ...this.initialState 29 | }; 30 | 31 | refresh = () => { 32 | this.setState({ 33 | ...this.initialState 34 | }) 35 | } 36 | 37 | onUsernameChange = (e) => { 38 | this.setState({ username: e.target.value }) 39 | } 40 | 41 | onPasswordChange = (e) => { 42 | this.setState({ password: e.target.value }) 43 | } 44 | 45 | redirectToSystemPage = () => { 46 | const { navigate } = this.props; 47 | const redirectPath = '/system/user-manage'; 48 | navigate(`${redirectPath}`); 49 | } 50 | 51 | processLogin = () => { 52 | const { username, password } = this.state; 53 | 54 | const { adminLoginSuccess, adminLoginFail } = this.props; 55 | let loginBody = { 56 | username: 'admin', 57 | password: '123456' 58 | } 59 | //sucess 60 | let adminInfo = { 61 | "tlid": "0", 62 | "tlfullname": "Administrator", 63 | "custype": "A", 64 | "accessToken": "eyJhbGciOiJIU" 65 | } 66 | 67 | adminLoginSuccess(adminInfo); 68 | this.refresh(); 69 | this.redirectToSystemPage(); 70 | try { 71 | adminService.login(loginBody) 72 | } catch (e) { 73 | console.log('error login : ', e) 74 | } 75 | 76 | } 77 | 78 | handlerKeyDown = (event) => { 79 | const keyCode = event.which || event.keyCode; 80 | if (keyCode === KeyCodeUtils.ENTER) { 81 | event.preventDefault(); 82 | if (!this.btnLogin.current || this.btnLogin.current.disabled) return; 83 | this.btnLogin.current.click(); 84 | } 85 | }; 86 | 87 | componentDidMount() { 88 | document.addEventListener('keydown', this.handlerKeyDown); 89 | } 90 | 91 | componentWillUnmount() { 92 | document.removeEventListener('keydown', this.handlerKeyDown); 93 | // fix Warning: Can't perform a React state update on an unmounted component 94 | this.setState = (state, callback) => { 95 | return; 96 | }; 97 | } 98 | 99 | render() { 100 | const { username, password, loginError } = this.state; 101 | const { lang } = this.props; 102 | 103 | return ( 104 |
    105 |
    106 |
    107 |

    108 | 109 |

    110 |
    111 | this 112 | 121 |
    122 | 123 |
    124 | this 125 | 134 |
    135 | 136 | {loginError !== '' && ( 137 |
    138 | {loginError} 139 |
    140 | )} 141 | 142 |
    143 | 151 |
    152 |
    153 |
    154 |
    155 | ) 156 | } 157 | } 158 | 159 | const mapStateToProps = state => { 160 | return { 161 | lang: state.app.language 162 | }; 163 | }; 164 | 165 | const mapDispatchToProps = dispatch => { 166 | return { 167 | navigate: (path) => dispatch(push(path)), 168 | adminLoginSuccess: (adminInfo) => dispatch(actions.adminLoginSuccess(adminInfo)), 169 | adminLoginFail: () => dispatch(actions.adminLoginFail()), 170 | }; 171 | }; 172 | 173 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 174 | -------------------------------------------------------------------------------- /src/routes/Login.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/common.scss"; 2 | 3 | .login-wrapper { 4 | .form-signin input[type=email], 5 | .form-signin input[type=password], 6 | .form-signin input[type=text], 7 | .form-signin button { 8 | -moz-box-sizing: border-box; 9 | -webkit-box-sizing: border-box; 10 | box-sizing: border-box; 11 | display: block; 12 | height: 55px; 13 | position: relative; 14 | width: 100%; 15 | z-index: 1; 16 | } 17 | 18 | .form-signin .form-control:focus { 19 | border-color: rgb(104, 145, 162); 20 | -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162); 21 | box-shadow: inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgb(104, 145, 162); 22 | outline: 0; 23 | } 24 | 25 | .login-container { 26 | height: 100vh; 27 | margin: auto 0 auto auto; 28 | padding: auto; 29 | width: 100%; 30 | } 31 | //huy.quang: disable default eye icon on ie < 10 32 | input::-ms-reveal, 33 | input::-ms-clear 34 | { 35 | display: none; 36 | } 37 | .login-container { 38 | padding: 100px 0 0 0; 39 | } 40 | .form_login { 41 | background: $colormain; 42 | border:none; 43 | border-radius: 8px; -webkit-border-radius:8px; 44 | height: 475px; 45 | margin: 50px auto 0; 46 | max-width: 470px;padding: 20px 50px; 47 | padding: 20px 50px; 48 | .title { 49 | color: $common-text-color; 50 | background-color: transparent; 51 | font-size: 18px; 52 | font-weight: bold; 53 | line-height: 1.4; 54 | margin: 20px 0 20px 0; 55 | text-align: center; 56 | text-transform: uppercase; 57 | } 58 | .error-msg { 59 | color:$common-error; 60 | margin-top:10px; 61 | text-align: center 62 | } 63 | .notify-msg { 64 | color:$common-notify; 65 | margin-top:10px; 66 | text-align: center 67 | } 68 | .form-group { 69 | input[type=submit], input[type=button] { 70 | &:focus, &:hover { 71 | background: $common-btn-deny; 72 | border: 1px solid $common-btn-deny; 73 | filter: brightness(120%); 74 | } 75 | } 76 | margin-bottom:20px; 77 | position: relative; 78 | &.separator { 79 | background: $common-text-color; 80 | border: none; 81 | height: 1px; 82 | padding: 0 50px; 83 | } 84 | .form-control{ 85 | background: $common-text-color; 86 | border: 1px solid rgba(0, 0, 0, 0.8); 87 | border-radius: 3px; 88 | -webkit-border-radius: 3px; 89 | box-sizing: border-box; 90 | color: rgba(0,0,0,0.8); 91 | height: 40px; 92 | margin-bottom: 0; 93 | &:focus, &:hover{ 94 | border: 3px solid $input-focus; 95 | } 96 | } 97 | &.icon-true { 98 | .form-control { 99 | background: $common-text-color; 100 | padding-left: 40px; 101 | width: 100%; 102 | } 103 | .icon { 104 | left: 0; 105 | position: absolute;top: 0; 106 | } 107 | } 108 | .error { 109 | display: none; 110 | text-align: right 111 | } 112 | } 113 | .login{ 114 | .btn{ 115 | background: $common-btn-confirm; 116 | color: #fff; 117 | display: block; 118 | font-size: 14px; 119 | height: 40px; 120 | margin-bottom: 13px; 121 | width: 100%; 122 | &.dn { 123 | background: $common-btn-confirm; 124 | } 125 | &.dk { 126 | background: $common-btn-confirm; 127 | } 128 | } 129 | } 130 | } 131 | .login-error { 132 | width: 100%; 133 | margin-bottom: 10px; 134 | text-align: center; 135 | .login-error-message { 136 | color: $common-error; 137 | } 138 | } 139 | 140 | /*=============END FOOTER==========*/ 141 | 142 | /*=============MENU MOBILE==========*/ 143 | /*=============END MENU MOBILE==========*/ 144 | 145 | /*RESPONSIVE*/ 146 | @media screen and (min-device-width: 1280px) and (max-device-width: 2400px) { 147 | .login-container { 148 | padding: 30px 0 0 0; 149 | } 150 | } 151 | @media (max-width: 1600px){ 152 | .login-container { 153 | padding: 100px 0 0 0; 154 | } 155 | .form_login{ 156 | height: 475px; 157 | max-width: 360px; 158 | .form-group { 159 | margin-bottom: 20px; 160 | } 161 | } 162 | } 163 | @media (max-width: 1479px){ 164 | .login-container { 165 | padding: 80px 0 0 0; 166 | } 167 | .form_login{ 168 | height: 475px; 169 | max-width: 360px; 170 | .form-group { 171 | margin-bottom: 20px; 172 | } 173 | } 174 | } 175 | @media (max-width: 1380px){ 176 | .form_login{ 177 | height: 475px; 178 | max-width: 360px; 179 | .form-group { 180 | margin-bottom: 20px; 181 | } 182 | } 183 | } 184 | @media (max-width: 1280px){ 185 | .login-container { 186 | padding: 30px 0 0 0; 187 | } 188 | .form_login{ 189 | height: 475px; 190 | max-width: 360px; 191 | .form-group { 192 | margin-bottom: 20px; 193 | } 194 | } 195 | } 196 | @media (max-width: 960px){ 197 | .form_login { 198 | margin: auto 199 | } 200 | .login-container { 201 | padding: 40px 30px 0 30px; 202 | } 203 | } 204 | 205 | @media (max-width: 740px){ 206 | .form_login { 207 | margin: auto 208 | } 209 | .login-container { 210 | padding: 40px 30px 0 30px; 211 | } 212 | } 213 | 214 | @media (max-width: 590px){ 215 | .form_login{ 216 | margin: auto 217 | } 218 | .login-container { 219 | padding: 40px 30px 0 30px; 220 | } 221 | } 222 | @media (max-width: 420px){ 223 | .form_login{ 224 | margin: auto 225 | } 226 | .login-container { 227 | padding: 40px 30px 0 30px; 228 | } 229 | } 230 | @media (max-width: 340px){ 231 | .form_login{ 232 | margin: auto 233 | } 234 | .login-container { 235 | padding: 40px 30px 0 30px; 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/routes/System.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from "react-redux"; 3 | import { Redirect, Route, Switch } from 'react-router-dom'; 4 | import UserManage from '../containers/System/UserManage'; 5 | import ProductManage from '../containers/System/ProductManage'; 6 | import RegisterPackageGroupOrAcc from '../containers/System/RegisterPackageGroupOrAcc'; 7 | 8 | class System extends Component { 9 | render() { 10 | const { systemMenuPath } = this.props; 11 | return ( 12 |
    13 |
    14 | 15 | 16 | 17 | 18 | { return () }} /> 19 | 20 |
    21 |
    22 | ); 23 | } 24 | } 25 | 26 | const mapStateToProps = state => { 27 | return { 28 | systemMenuPath: state.app.systemMenuPath 29 | }; 30 | }; 31 | 32 | const mapDispatchToProps = dispatch => { 33 | return { 34 | }; 35 | }; 36 | 37 | export default connect(mapStateToProps, mapDispatchToProps)(System); 38 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/services/adminService.js: -------------------------------------------------------------------------------- 1 | import axios from '../axios'; 2 | import * as queryString from 'query-string'; 3 | 4 | const adminService = { 5 | 6 | /** 7 | * Đăng nhập hệ thống 8 | * { 9 | * "username": "string", 10 | * "password": "string" 11 | * } 12 | */ 13 | login(loginBody) { 14 | return axios.post(`/admin/login`, loginBody) 15 | }, 16 | 17 | }; 18 | 19 | export default adminService; -------------------------------------------------------------------------------- /src/services/index.js: -------------------------------------------------------------------------------- 1 | export { default as adminService } from './adminService'; -------------------------------------------------------------------------------- /src/store/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | const actionTypes = Object.freeze({ 2 | //app 3 | APP_START_UP_COMPLETE: 'APP_START_UP_COMPLETE', 4 | SET_CONTENT_OF_CONFIRM_MODAL: 'SET_CONTENT_OF_CONFIRM_MODAL', 5 | 6 | //admin 7 | ADMIN_LOGIN_SUCCESS: 'ADMIN_LOGIN_SUCCESS', 8 | ADMIN_LOGIN_FAIL: 'ADMIN_LOGIN_FAIL', 9 | PROCESS_LOGOUT: 'PROCESS_LOGOUT', 10 | 11 | //user 12 | ADD_USER_SUCCESS: 'ADD_USER_SUCCESS', 13 | }) 14 | 15 | export default actionTypes; -------------------------------------------------------------------------------- /src/store/actions/adminActions.js: -------------------------------------------------------------------------------- 1 | import actionTypes from './actionTypes'; 2 | 3 | export const adminLoginSuccess = (adminInfo) => ({ 4 | type: actionTypes.ADMIN_LOGIN_SUCCESS, 5 | adminInfo: adminInfo 6 | }) 7 | 8 | export const adminLoginFail = () => ({ 9 | type: actionTypes.ADMIN_LOGIN_FAIL 10 | }) 11 | 12 | export const processLogout = () => ({ 13 | type: actionTypes.PROCESS_LOGOUT 14 | }) -------------------------------------------------------------------------------- /src/store/actions/appActions.js: -------------------------------------------------------------------------------- 1 | import actionTypes from './actionTypes'; 2 | 3 | export const appStartUpComplete = () => ({ 4 | type: actionTypes.APP_START_UP_COMPLETE 5 | }); 6 | 7 | export const setContentOfConfirmModal = (contentOfConfirmModal) => ({ 8 | type: actionTypes.SET_CONTENT_OF_CONFIRM_MODAL, 9 | contentOfConfirmModal: contentOfConfirmModal 10 | }); -------------------------------------------------------------------------------- /src/store/actions/index.js: -------------------------------------------------------------------------------- 1 | export * from './appActions' 2 | export * from './adminActions' 3 | export * from './userActions' -------------------------------------------------------------------------------- /src/store/actions/userActions.js: -------------------------------------------------------------------------------- 1 | import actionTypes from './actionTypes'; 2 | 3 | export const addUserSuccess = () => ({ 4 | type: actionTypes.ADD_USER_SUCCESS 5 | }) -------------------------------------------------------------------------------- /src/store/reducers/adminReducer.js: -------------------------------------------------------------------------------- 1 | import actionTypes from '../actions/actionTypes'; 2 | 3 | const initialState = { 4 | isLoggedIn: false, 5 | adminInfo: null 6 | } 7 | 8 | const appReducer = (state = initialState, action) => { 9 | switch (action.type) { 10 | case actionTypes.ADMIN_LOGIN_SUCCESS: 11 | return { 12 | ...state, 13 | isLoggedIn: true, 14 | adminInfo: action.adminInfo 15 | } 16 | case actionTypes.ADMIN_LOGIN_FAIL: 17 | return { 18 | ...state, 19 | isLoggedIn: false, 20 | adminInfo: null 21 | } 22 | case actionTypes.PROCESS_LOGOUT: 23 | return { 24 | ...state, 25 | isLoggedIn: false, 26 | adminInfo: null 27 | } 28 | default: 29 | return state; 30 | } 31 | } 32 | 33 | export default appReducer; -------------------------------------------------------------------------------- /src/store/reducers/appReducer.js: -------------------------------------------------------------------------------- 1 | import actionTypes from '../actions/actionTypes'; 2 | 3 | const initContentOfConfirmModal = { 4 | isOpen: false, 5 | messageId: "", 6 | handleFunc: null, 7 | dataFunc: null 8 | } 9 | 10 | const initialState = { 11 | started: true, 12 | language: 'vi', 13 | systemMenuPath: '/system/user-manage', 14 | contentOfConfirmModal: { 15 | ...initContentOfConfirmModal 16 | } 17 | } 18 | 19 | const appReducer = (state = initialState, action) => { 20 | switch (action.type) { 21 | case actionTypes.APP_START_UP_COMPLETE: 22 | return { 23 | ...state, 24 | started: true 25 | } 26 | case actionTypes.SET_CONTENT_OF_CONFIRM_MODAL: 27 | return { 28 | ...state, 29 | contentOfConfirmModal: { 30 | ...state.contentOfConfirmModal, 31 | ...action.contentOfConfirmModal 32 | } 33 | } 34 | default: 35 | return state; 36 | } 37 | } 38 | 39 | export default appReducer; -------------------------------------------------------------------------------- /src/store/reducers/rootReducer.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import { connectRouter } from 'connected-react-router'; 3 | 4 | import appReducer from "./appReducer"; 5 | import adminReducer from "./adminReducer"; 6 | import userReducer from "./userReducer"; 7 | 8 | import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; 9 | import storage from 'redux-persist/lib/storage'; 10 | import { persistReducer } from 'redux-persist'; 11 | 12 | const persistCommonConfig = { 13 | storage: storage, 14 | stateReconciler: autoMergeLevel2, 15 | }; 16 | 17 | const adminPersistConfig = { 18 | ...persistCommonConfig, 19 | key: 'admin', 20 | whitelist: ['isLoggedIn', 'adminInfo'] 21 | }; 22 | 23 | export default (history) => combineReducers({ 24 | router: connectRouter(history), 25 | admin: persistReducer(adminPersistConfig, adminReducer), 26 | user: userReducer, 27 | app: appReducer 28 | }) -------------------------------------------------------------------------------- /src/store/reducers/userReducer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haryphamdev/Frontend-React.JS-QuickStart/b0f772df40c293463f1958c50c13c71534c3c04e/src/store/reducers/userReducer.js -------------------------------------------------------------------------------- /src/styles/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * General styles 3 | */ 4 | body, html { 5 | background-image: "#fff"; 6 | background-repeat: no-repeat; 7 | background-size: cover; 8 | font-family: $fontmain; 9 | font-size: $base-size - 2px; 10 | height: 100vh; 11 | overflow: hidden; 12 | } 13 | 14 | .btn { 15 | padding: 0; 16 | border: none; 17 | cursor: pointer; 18 | font-weight: 700; 19 | height: 30px; 20 | line-height: 30px; 21 | -moz-user-select: none; 22 | -webkit-user-select: none; 23 | user-select: none; 24 | i { 25 | font-size: $base-size - 3px; 26 | } 27 | } 28 | 29 | .btn-edit, 30 | .btn-add, 31 | .btn-delete, 32 | .btn-assign { 33 | background-color: $colormain; 34 | color: $common-text-color; 35 | &:hover { 36 | color: $common-text-color; 37 | background-color: darken($colormain, 5); 38 | } 39 | } 40 | 41 | #password:invalid { 42 | font-family: $fontmain; // Fixed TH hien thi placeholder cho EDGE 43 | } 44 | #password:-webkit-input-placeholder { 45 | font-family: $fontmain; // CHROME 1 46 | } 47 | #password::-webkit-input-placeholder { 48 | font-family: $fontmain; //CHROME 2 EDGE 1 49 | } 50 | #password:-moz-placeholder { 51 | font-family: $fontmain; // Firefox 4-18 52 | } 53 | #password::-moz-placeholder { 54 | font-family: $fontmain; // Firefox 19+ 55 | } 56 | #password:-ms-input-placeholder { 57 | font-family: $fontmain; // IE 10-11 58 | } 59 | #password::-ms-input-placeholder { 60 | font-family: $fontmain; // EDGE 2 61 | } 62 | #password::placeholder { 63 | font-family: $fontmain; 64 | } 65 | 66 | .content-container { 67 | .title { 68 | text-align: center; 69 | text-transform: uppercase; 70 | font-weight: bold; 71 | font-size: $base-size + 4px; 72 | margin-top: 15px; 73 | color: $colormain; 74 | } 75 | .content { 76 | margin: 15px 30px; 77 | box-shadow: 2px 2px 4px 0 $box-shadow-color; 78 | -webkit-box-shadow: 2px 2px 4px 0 $box-shadow-color; 79 | } 80 | } 81 | 82 | .modal { 83 | .modal-dialog { 84 | .modal-content { 85 | .modal-header { 86 | padding: 0 10px; 87 | height: 35px; 88 | background-color: $colormain; 89 | color: $common-text-color; 90 | .modal-title { 91 | line-height: 35px; 92 | font-weight: 500; 93 | font-size: $base-size + 1px; 94 | } 95 | .btn-close { 96 | position: absolute; 97 | color: $common-text-color; 98 | i { 99 | line-height: 35px; 100 | font-size: $base-size + 2px; 101 | } 102 | } 103 | } 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /src/styles/_form.scss: -------------------------------------------------------------------------------- 1 | @import "common"; 2 | 3 | .custom-form-group { 4 | 5 | .custom-form-control { 6 | background-color: $common-white; 7 | border: 1px solid $border; 8 | &.readonly { 9 | cursor: not-allowed; 10 | background-color: darken($common-white, 5); 11 | } 12 | &:focus { 13 | outline: none; 14 | border: 1px solid darken($border, 50); 15 | } 16 | } 17 | 18 | select.custom-form-control { 19 | background-image: $dropdown-image; 20 | } 21 | 22 | margin-bottom: 10px; 23 | 24 | label { 25 | margin-bottom: 1px; 26 | } 27 | 28 | .custom-form-control { 29 | display: block; 30 | width: 100%; 31 | height: 28px; 32 | padding: 0 5px; 33 | line-height: 26px; 34 | border-radius: 5px; 35 | } 36 | 37 | textarea.custom-form-control { 38 | min-height: 28px; 39 | height: auto; 40 | } 41 | 42 | select.custom-form-control { 43 | -webkit-appearance: none; 44 | -moz-appearance: none; 45 | 46 | padding-right: 15px; 47 | 48 | background-position: right top; 49 | background-repeat: no-repeat; 50 | background-size: 15px 100%; 51 | 52 | cursor: pointer; 53 | 54 | &:disabled { 55 | cursor: not-allowed; 56 | } 57 | } 58 | 59 | .custom-checkbox-control { 60 | line-height: 28px; 61 | 62 | input { 63 | cursor: pointer; 64 | } 65 | 66 | label { 67 | margin-left: 5px; 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $base-size: 16px; 2 | $lh: 1.313; 3 | $fontmain:"Helvetica", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 4 | $colormain:#0071ba; 5 | $colorsub:#bb8d09; 6 | $common-text-color:white; 7 | $common-btn-confirm: #185ba8; 8 | $common-btn-deny: #bb8d09; 9 | $common-error: #ff9600; 10 | $common-notify: greenyellow; 11 | $common-green: #00C087; 12 | $common-red: #d01a1d; 13 | $common-white: #fff; 14 | $common-orange: #FFAC42; 15 | $border: #ccc; 16 | $text-in-light: #181818; 17 | 18 | $input-focus: #FFAC42; 19 | 20 | $footer-container: #f5f5f5; 21 | $footer-text: #262626; 22 | $footer-next-disable: #ccc; 23 | 24 | $bg-menu-color: #f5f5f5; 25 | $bg-scrollbar: #000; 26 | $bg-table-color: #f5f5f5; 27 | $box-shadow-color: #0000004d; 28 | 29 | $dropdown-image: url("../assets/images/dropdown.svg"); 30 | 31 | //date picker 32 | $date-picker-bg: $common-white; 33 | $date-picker-border: darken(#3f4458, 50%); 34 | $date-picker-arrow-hover: $common-orange; 35 | $date-picker-month-fg: #000; 36 | $date-picker-month-bg: $common-white; 37 | $date-picker-weekdays-fg: #000; 38 | $date-picker-weekdays-bg: transparent; 39 | $date-picker-day-fg: #000; 40 | $date-picker-day-hover-bg: darken($common-white, 25%); 41 | $date-picker-today-border: $common-orange; 42 | $date-picker-today-bg: $common-orange; 43 | $date-picker-today-fg: #000; 44 | $date-picker-day-selected-fg: $common-white; -------------------------------------------------------------------------------- /src/styles/common.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | 3 | .text-center { 4 | text-align: center !important; 5 | } 6 | 7 | .text-left { 8 | text-align: left !important; 9 | } 10 | 11 | .text-right { 12 | text-align: right !important; 13 | } 14 | 15 | .text-red { 16 | color: $common-red !important; 17 | } -------------------------------------------------------------------------------- /src/styles/styles.scss: -------------------------------------------------------------------------------- 1 | @import "variables"; 2 | @import "../../node_modules/bootstrap/scss/bootstrap"; 3 | 4 | $fa-font-path : "~@fortawesome/fontawesome-free-webfonts/webfonts"; 5 | @import "~@fortawesome/fontawesome-free-webfonts/scss/fontawesome.scss"; 6 | @import "~@fortawesome/fontawesome-free-webfonts/scss/fa-solid.scss"; 7 | @import "~@fortawesome/fontawesome-free-webfonts/scss/fa-regular.scss"; 8 | @import "~@fortawesome/fontawesome-free-webfonts/scss/fa-brands.scss"; 9 | 10 | @import "base"; 11 | @import "form"; -------------------------------------------------------------------------------- /src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "add": "Add", 4 | "edit": "Edit", 5 | "delete": "Delete", 6 | "decentralize": "Decentralize", 7 | "close": "Close", 8 | "save": "Save", 9 | "refresh": "Refresh", 10 | "accept": "Accept", 11 | "confirm": "Confirm", 12 | "confirm-this-task": "Are you sure to process this task?", 13 | "fail-to-load-data": "Fail to load data", 14 | "unknown-error": "There was an error", 15 | "internal-server-error": "Internal server error", 16 | "bad-request": "Bad request", 17 | "forbiden-request": "Invalid input data", 18 | "fail-to-load-all-code": "Fail to load data from allcode", 19 | "sum": "Sum", 20 | "date-range-invalid": "Date range invalid" 21 | }, 22 | "login": { 23 | "login": "Login", 24 | "username": "Username", 25 | "password": "Password", 26 | "userpass-wrong": "Invalid username or password!" 27 | }, 28 | "menu": { 29 | "system": { 30 | "header": "System", 31 | "system-administrator": { 32 | "header": "System Administrator", 33 | "user-manage": "User manage", 34 | "product-manage": "Package manage", 35 | "register-package-group-or-account": "Register service package for group/account" 36 | }, 37 | "system-parameter": { 38 | "header": "System-parameter" 39 | } 40 | } 41 | }, 42 | "system": { 43 | "user-manage": { 44 | "fail-to-load-fouser": "Fail to load system users list", 45 | "user-id": "User ID", 46 | "usertype": "User type", 47 | "username": "Username", 48 | "fullname": "Fullname", 49 | "mobile": "Phone", 50 | "email": "Email", 51 | "status": "Status", 52 | "add-user": "Add user", 53 | "edit-user": "Edit user information", 54 | "password": "Password", 55 | "retype-password": "Retype password", 56 | "add-user-success": "Add user successfully", 57 | "edit-user-success": "Change user information successfully", 58 | "add-user-fail": "Fail to add user", 59 | "edit-user-fail": "Fail to change user information", 60 | "del-user-success": "Delete user successfully", 61 | "del-user-fail": "Fail to delete user", 62 | "invalid-input": { 63 | "username": "Please fill in username", 64 | "fullname": "Please fill in fullname", 65 | "password": "Please fill in password" 66 | }, 67 | "sure-delete-user": "Are you sure to delete this user?" 68 | }, 69 | "product-manage": { 70 | "fail-to-load-foprtype": "Fail to load packages list", 71 | "prid": "Package ID", 72 | "prname": "Package name", 73 | "prtype": "Package type", 74 | "status": "Status", 75 | "description": "Description", 76 | "effective-date": "Effective date", 77 | "expiration-date": "Expiration date", 78 | "add-product": "Add package", 79 | "add-product-success": "Add package successfully", 80 | "add-product-fail": "Fail to add package", 81 | "edit-product": "Edit package information", 82 | "package-decentralize": "Package decentralize", 83 | "edit-product-success": "Edit package information successfully", 84 | "edit-product-fail": "Fail to edit package information", 85 | "delete-product-success": "Delete package successfully", 86 | "delete-product-fail": "Fail to delete package", 87 | "invalid-input": { 88 | "prid": "Please fill in package ID", 89 | "prname": "Please fill in package name" 90 | }, 91 | "fail-to-load-decentralize-info": "Fail to load decentralize info", 92 | "save-decentralize-success": "Save decentralization successfully", 93 | "save-decentralize-fail": "Fail to save decentralization", 94 | "sure-delete-package": "Are you sure to delete this package?" 95 | }, 96 | "register-package-group-or-acc": { 97 | "assign-service-package-to-customer": "Assign service package to customer", 98 | "package-name": "Package name", 99 | "assign-type": "Assign type", 100 | "cust-acc-group": "Customer/account group", 101 | "effective-date": "Effective date", 102 | "expiration-date": "Expiration date", 103 | "assign": "Assign", 104 | "manage-assign-groups": "Manage assign groups", 105 | "group-cust-acc-name": "Group/customer account name", 106 | "del-assign": "Del assign", 107 | "fail-to-load-manage-assign-groups-list": "Fail to load manage assign groups list", 108 | "invalid-selected": { 109 | "currentfoProduct": "Invalid package", 110 | "currentAssignType": "Invalid assign type", 111 | "currentFoUser": "Invalid customer/account group" 112 | }, 113 | "assign-service-package-to-customer-success": "Assign service package to customer successfully", 114 | "assign-service-package-to-customer-fail": "Fail to assign service package to customer", 115 | "remove-assign-success": "Unassign service package to customer successfully", 116 | "remove-assign-fail": "Fail to unassign service package to customer", 117 | "sure-delete-assign": "Are you sure to unassign service package to customer?" 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/translations/vi.json: -------------------------------------------------------------------------------- 1 | { 2 | "common": { 3 | "add": "Thêm", 4 | "edit": "Sửa", 5 | "delete": "Xóa", 6 | "decentralize": "Phân quyền", 7 | "close": "Đóng", 8 | "save": "Lưu", 9 | "refresh": "Refresh", 10 | "accept": "Chấp nhận", 11 | "confirm": "Xác nhận", 12 | "confirm-this-task": "Bạn có chắc chắn thực hiện tác vụ này?", 13 | "fail-to-load-data": "Không thể tải về dữ liệu", 14 | "unknown-error": "Có lỗi xảy ra", 15 | "internal-server-error": "Lỗi kết nối", 16 | "bad-request": "Yêu cầu không hợp lệ", 17 | "forbiden-request": "Thông tin truyền vào không hợp lệ", 18 | "fail-to-load-all-code": "Không thể tải về dữ liệu allcode", 19 | "sum": "Tổng", 20 | "date-range-invalid": "Khoảng thời gian không hợp lệ" 21 | }, 22 | "login": { 23 | "login": "Đăng nhập", 24 | "username": "Tên đăng nhập", 25 | "password": "Mật khẩu", 26 | "userpass-wrong": "Sai tên đăng nhập hoặc mật khẩu!" 27 | }, 28 | "menu": { 29 | "system": { 30 | "header": "Hệ thống", 31 | "system-administrator": { 32 | "header": "Quản trị hệ thống", 33 | "user-manage": "Quản lý người sử dụng", 34 | "product-manage": "Quản lý gói", 35 | "register-package-group-or-account": "Đăng ký gói dịch vụ cho nhóm/tài khoản" 36 | }, 37 | "system-parameter": { 38 | "header": "Tham số hệ thống" 39 | } 40 | } 41 | }, 42 | "system": { 43 | "user-manage": { 44 | "fail-to-load-fouser": "Không thể tải về danh sách người dùng", 45 | "user-id": "Mã người sử dụng", 46 | "usertype": "Loại người dùng", 47 | "username": "Tên người dùng", 48 | "fullname": "Tên đầy đủ", 49 | "mobile": "Số điện thoại", 50 | "email": "Email", 51 | "status": "Trạng thái", 52 | "add-user": "Thêm người dùng", 53 | "edit-user": "Sửa thông tin người dùng", 54 | "password": "Mật khẩu", 55 | "retype-password": "Nhập lại mật khẩu", 56 | "add-user-success": "Thêm người dùng thành công", 57 | "edit-user-success": "Thay đổi thông tin người dùng thành công", 58 | "add-user-fail": "Thêm người dùng thất bại", 59 | "edit-user-fail": "Thay đổi thông tin người dùng thất bại", 60 | "del-user-success": "Xóa người dùng thành công", 61 | "del-user-fail": "Xóa người dùng thất bại", 62 | "invalid-input": { 63 | "username": "Quý khách vui lòng nhập tên người dùng", 64 | "fullname": "Quý khách vui lòng nhập tên đầy đủ", 65 | "password": "Quý khách vui lòng nhập mật khẩu" 66 | }, 67 | "sure-delete-user": "Bạn có chắc chắn muốn xóa người dùng này?" 68 | }, 69 | "product-manage": { 70 | "fail-to-load-foprtype": "Không thể tải về danh sách gói", 71 | "prid": "Mã gói", 72 | "prname": "Tên gói", 73 | "prtype": "Loại gói", 74 | "status": "Trạng thái", 75 | "description": "Diễn giải", 76 | "effective-date": "Ngày hiệu lực", 77 | "expiration-date": "Ngày hết hiệu lực", 78 | "add-product": "Thêm gói", 79 | "add-product-success": "Thêm gói thành công", 80 | "add-product-fail": "Thêm gói thất bại", 81 | "edit-product": "Sửa thông tin gói", 82 | "package-decentralize": "Phân quyền chức năng", 83 | "edit-product-success": "Thay đổi thông tin gói thành công", 84 | "edit-product-fail": "Thay đổi thông tin gói thất bại", 85 | "delete-product-success": "Xóa gói thành công", 86 | "delete-product-fail": "Xóa gói thất bại", 87 | "invalid-input": { 88 | "prid": "Quý khách vui lòng nhập mã gói", 89 | "prname": "Quý khách vui lòng nhập tên gói" 90 | }, 91 | "fail-to-load-decentralize-info": "Không thể tải về thông tin phân quyền chức năng", 92 | "save-decentralize-success": "Lưu phân quyền chức năng thành công", 93 | "save-decentralize-fail": "Lưu phân quyền chức năng thất bại", 94 | "sure-delete-package": "Bạn có chắc chắn muốn xóa gói này?" 95 | }, 96 | "register-package-group-or-acc": { 97 | "assign-service-package-to-customer": "Gán gói dịch vụ cho khách hàng", 98 | "package-name": "Tên gói", 99 | "assign-type": "Loại gán", 100 | "cust-acc-group": "Nhóm KH/Tài khoản", 101 | "effective-date": "Ngày hiệu lực", 102 | "expiration-date": "Ngày hết hiệu lực", 103 | "assign": "Gán", 104 | "manage-assign-groups": "Quản lý gán nhóm", 105 | "group-cust-acc-name": "Tên nhóm/tài khoản KH", 106 | "del-assign": "Xóa gán", 107 | "fail-to-load-manage-assign-groups-list": "Không thể tải về thông tin quản lý gán nhóm", 108 | "invalid-selected": { 109 | "currentfoProduct": "Gói đang chọn không hợp lệ", 110 | "currentAssignType": "Loại gán không hợp lệ", 111 | "currentFoUser": "Nhóm KH/Tài khoản không hợp lệ" 112 | }, 113 | "assign-service-package-to-customer-success": "Gán gói dịch vụ cho khách hàng thành công", 114 | "assign-service-package-to-customer-fail": "Gán gói dịch vụ cho khách hàng thất bại", 115 | "remove-assign-success": "Bỏ gán gói dịch vụ cho khách hàng thành công", 116 | "remove-assign-fail": "Bỏ gán gói dịch vụ cho khách hàng thất bại", 117 | "sure-delete-assign": "Bạn có chắc chắn muốn bỏ gán gói dịch vụ cho khách hàng?" 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/utils/CommonUtils.js: -------------------------------------------------------------------------------- 1 | class CommonUtils { 2 | static isNumber1 (number) { 3 | if (number === 1) return true; 4 | return false; 5 | } 6 | } 7 | 8 | export default CommonUtils; -------------------------------------------------------------------------------- /src/utils/KeyCodeUtils.js: -------------------------------------------------------------------------------- 1 | class KeyCodeUtils { 2 | 3 | static UP = 38; 4 | 5 | static DOWN = 40; 6 | 7 | static TAB = 9; 8 | 9 | static ENTER = 13; 10 | 11 | static E = 69; 12 | 13 | static ESCAPE = 27; 14 | 15 | static isNavigation(e) { 16 | return (e >= 33 && e <= 40) || e === 9 || e === 8 || e === 46 || e === 14 || e === 13; 17 | } 18 | 19 | static isNumeric(e) { 20 | return (e >= 48 && e <= 57) || (e >= 96 && e <= 105); 21 | } 22 | static isAlphabetic(e) { 23 | return (e >= 65 && e <= 90); 24 | } 25 | static isDecimal(e) { 26 | return e === 190 || e === 188 || e === 108 || e === 110; 27 | } 28 | 29 | static isDash(e) { 30 | return e === 109 || e === 189; 31 | } 32 | } 33 | 34 | export default KeyCodeUtils; -------------------------------------------------------------------------------- /src/utils/LanguageUtils.js: -------------------------------------------------------------------------------- 1 | import messages_vi from '../translations/vi.json'; 2 | import messages_en from '../translations/en.json'; 3 | 4 | const flattenMessages = ((nestedMessages, prefix = '') => { 5 | if (nestedMessages == null) { 6 | return {} 7 | } 8 | return Object.keys(nestedMessages).reduce((messages, key) => { 9 | const value = nestedMessages[key]; 10 | const prefixedKey = prefix ? `${prefix}.${key}` : key; 11 | 12 | if (typeof value === 'string') { 13 | Object.assign(messages, {[prefixedKey]: value}) 14 | } else { 15 | Object.assign(messages, flattenMessages(value, prefixedKey)) 16 | } 17 | 18 | return messages 19 | }, {}) 20 | }); 21 | 22 | const messages = { 23 | 'vi': flattenMessages(messages_vi), 24 | 'en': flattenMessages(messages_en), 25 | }; 26 | 27 | export default class LanguageUtils { 28 | static getMessageByKey(key, lang) { 29 | return messages[lang][key] 30 | } 31 | 32 | static getFlattenedMessages() { 33 | return messages; 34 | } 35 | } -------------------------------------------------------------------------------- /src/utils/ToastUtil.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toast } from 'react-toastify'; 3 | import axios from 'axios'; 4 | 5 | import CustomToast from "../components/CustomToast"; 6 | 7 | const TYPE_SUCCESS = 'SUCCESS'; 8 | const TYPE_INFO = 'INFO'; 9 | const TYPE_WARN = 'WARN'; 10 | const TYPE_ERROR = 'ERROR'; 11 | 12 | class ToastUtil { 13 | 14 | static success(title, message) { 15 | this.show(TYPE_SUCCESS, title, message); 16 | } 17 | 18 | static info(title, message) { 19 | this.show(TYPE_INFO, title, message); 20 | } 21 | 22 | static warn(title, message) { 23 | this.show(TYPE_WARN, title, message); 24 | } 25 | 26 | static error(title, message) { 27 | this.show(TYPE_ERROR, title, message); 28 | } 29 | 30 | static successRaw(title, message) { 31 | this.show(TYPE_SUCCESS, title, message, true); 32 | } 33 | 34 | static errorRaw(title, message, autoCloseDelay = 3000) { 35 | this.show(TYPE_ERROR, title, message, true, autoCloseDelay); 36 | } 37 | static errorApi(error, title = 'common.fail-to-load-data', autoCloseDelay = 3000) { 38 | if (axios.isCancel(error)) { 39 | // Do nothing if request was cancelled 40 | return; 41 | } 42 | let message = null; 43 | let messageId = 'common.unknown-error'; 44 | if (error.httpStatusCode >= 500) { 45 | messageId = 'common.internal-server-error'; 46 | } else if (error.httpStatusCode < 500 && error.httpStatusCode >= 400) { 47 | if (error.httpStatusCode === 400) { 48 | messageId = 'common.bad-request'; 49 | } else if (error.httpStatusCode === 403) { 50 | messageId = 'common.forbiden-request'; 51 | } 52 | } else { 53 | // Request fail even server was returned a success response 54 | if (error.errorMessage) { 55 | message = error.errorMessage 56 | } 57 | } 58 | toast.error(, { 59 | position: toast.POSITION.BOTTOM_RIGHT, 60 | pauseOnHover: true, 61 | autoClose: autoCloseDelay 62 | }); 63 | } 64 | 65 | static show(type, title, message, rawMessage = false, autoCloseDelay = 3000) { 66 | const content = ; 67 | const options = { 68 | position: toast.POSITION.BOTTOM_RIGHT, 69 | pauseOnHover: true, 70 | autoClose: autoCloseDelay 71 | }; 72 | 73 | switch (type) { 74 | case TYPE_SUCCESS: 75 | toast.success(content, options); 76 | break; 77 | case TYPE_INFO: 78 | toast.info(content, options); 79 | break; 80 | case TYPE_WARN: 81 | toast.warn(content, options); 82 | break; 83 | case TYPE_ERROR: 84 | toast.error(content, options); 85 | break; 86 | default: 87 | break; 88 | } 89 | } 90 | } 91 | 92 | export default ToastUtil; -------------------------------------------------------------------------------- /src/utils/constant.js: -------------------------------------------------------------------------------- 1 | export const path = { 2 | HOME: '/', 3 | LOGIN: '/login', 4 | LOG_OUT: '/logout', 5 | SYSTEM: '/system' 6 | }; 7 | 8 | export const languages = { 9 | VI: 'vi', 10 | EN: 'en' 11 | }; 12 | 13 | export const manageActions = { 14 | ADD: "ADD", 15 | EDIT: "EDIT", 16 | DELETE: "DELETE" 17 | }; 18 | 19 | export const dateFormat = { 20 | SEND_TO_SERVER: 'DD/MM/YYYY' 21 | }; 22 | 23 | export const YesNoObj = { 24 | YES: 'Y', 25 | NO: 'N' 26 | } -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './constant'; 2 | export {default as CommonUtils} from './CommonUtils'; 3 | export {default as KeyCodeUtils} from './KeyCodeUtils'; 4 | export {default as LanguageUtils} from './LanguageUtils'; 5 | export {default as ToastUtil} from './ToastUtil'; --------------------------------------------------------------------------------