[feat] Initial code
This commit is contained in:
commit
7dc01d5480
5
.eslintignore
Normal file
5
.eslintignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/config
|
||||||
|
/scripts
|
||||||
|
/libs
|
||||||
|
/src/services/Scripts/ElementWorkers/*
|
||||||
|
/.eslintrc.js
|
85
.eslintrc.js
Normal file
85
.eslintrc.js
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: [
|
||||||
|
'@typescript-eslint',
|
||||||
|
'import',
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
'airbnb',
|
||||||
|
'eslint-config-airbnb',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'indent': 'off',
|
||||||
|
'max-len' : 'off',
|
||||||
|
'semi': ['error'],
|
||||||
|
'camelcase': 'off',
|
||||||
|
'prefer-destructuring': 'off',
|
||||||
|
'global-require': 'off',
|
||||||
|
'prefer-template': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'react/no-array-index-key': 'off',
|
||||||
|
'react/jsx-fragments': 'off',
|
||||||
|
'react/jsx-props-no-spreading': 'off',
|
||||||
|
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx', '.tsx', '.ts'] }],
|
||||||
|
'jsx-a11y/control-has-associated-label': 'off',
|
||||||
|
'import/named': 'off',
|
||||||
|
'import/prefer-default-export': 'off',
|
||||||
|
'import/no-extraneous-dependencies': 'off',
|
||||||
|
'import/no-unresolved': [2, { ignore: ['\.png$'] }],
|
||||||
|
'import/no-dynamic-require': 'off',
|
||||||
|
'import/no-named-as-default': 'off',
|
||||||
|
'no-use-before-define': 'off',
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'no-useless-constructor': 'off',
|
||||||
|
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||||
|
'no-param-reassign': 'off',
|
||||||
|
'no-await-in-loop': 'off',
|
||||||
|
'no-loop-func': 'off',
|
||||||
|
'no-shadow': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-empty-interface': 'off',
|
||||||
|
'@typescript-eslint/indent': ['error', 2],
|
||||||
|
'@typescript-eslint/explicit-function-return-type': [1, { allowExpressions: true, allowTypedFunctionExpressions: true }],
|
||||||
|
'@typescript-eslint/no-unused-vars': [2],
|
||||||
|
// Additional
|
||||||
|
'@typescript-eslint/triple-slash-reference': 'off',
|
||||||
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
|
'@typescript-eslint/indent': 'off',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/no-unresolved': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 'off',
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
"import/resolver": {
|
||||||
|
"alias": {
|
||||||
|
"map": [
|
||||||
|
["@Base", "./src/base"],
|
||||||
|
["@Components", "./src/components"],
|
||||||
|
["@Reducers", "./src/reducers"],
|
||||||
|
["@API", "./src/api"],
|
||||||
|
["@Tools", "./src/tools"],
|
||||||
|
["@Hooks", "./src/hooks"],
|
||||||
|
["@Models", "./src/models"],
|
||||||
|
["@CSS", "./src/css"],
|
||||||
|
["@Services", "./src/services"],
|
||||||
|
["@Providers", "./src/providers"],
|
||||||
|
["@Env", "./src/env"],
|
||||||
|
["@Langs", "./src/langs"],
|
||||||
|
["@Mocks", "./src/mocks"],
|
||||||
|
["@Plugin", "./src/plugin"],
|
||||||
|
],
|
||||||
|
"extensions": [".js", ".jsx", '.ts', '.tsx', '.mjs', ".json"]
|
||||||
|
},
|
||||||
|
"node": {
|
||||||
|
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"import/parsers": {
|
||||||
|
'@typescript-eslint/parser': ['.ts', '.tsx', '.mjs'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
"browser": true,
|
||||||
|
"jest": true,
|
||||||
|
},
|
||||||
|
};
|
204
.gitattributes
vendored
Normal file
204
.gitattributes
vendored
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# Sync Line break
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Document Type
|
||||||
|
*.markdown text
|
||||||
|
*.md text
|
||||||
|
*.mdwn text
|
||||||
|
*.mdown text
|
||||||
|
*.mkd text
|
||||||
|
*.mkdn text
|
||||||
|
*.mdtxt text
|
||||||
|
*.mdtext text
|
||||||
|
*.txt text
|
||||||
|
AUTHORS text
|
||||||
|
CHANGELOG text
|
||||||
|
CHANGES text
|
||||||
|
CONTRIBUTING text
|
||||||
|
COPYING text
|
||||||
|
copyright text
|
||||||
|
*COPYRIGHT* text
|
||||||
|
INSTALL text
|
||||||
|
license text
|
||||||
|
LICENSE text
|
||||||
|
NEWS text
|
||||||
|
readme text
|
||||||
|
*README* text
|
||||||
|
TODO text
|
||||||
|
|
||||||
|
# Microsoft Word Type
|
||||||
|
*.doc diff=astextplain
|
||||||
|
*.DOC diff=astextplain
|
||||||
|
*.docx diff=astextplain
|
||||||
|
*.DOCX diff=astextplain
|
||||||
|
*.dot diff=astextplain
|
||||||
|
*.DOT diff=astextplain
|
||||||
|
*.pdf diff=astextplain
|
||||||
|
*.PDF diff=astextplain
|
||||||
|
*.rtf diff=astextplain
|
||||||
|
*.RTF diff=astextplain
|
||||||
|
|
||||||
|
# Code Type
|
||||||
|
*.bat text eol=lf
|
||||||
|
*.coffee text
|
||||||
|
*.css text eol=lf
|
||||||
|
*.htm text
|
||||||
|
*.html text eol=lf
|
||||||
|
*.inc text
|
||||||
|
*.ini text
|
||||||
|
*.js text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.less text
|
||||||
|
*.od text
|
||||||
|
*.onlydata text
|
||||||
|
*.php text
|
||||||
|
*.pl text
|
||||||
|
*.py text
|
||||||
|
*.rb text
|
||||||
|
*.sass text
|
||||||
|
*.scm text
|
||||||
|
*.scss text
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.sql text
|
||||||
|
*.styl text
|
||||||
|
*.tag text
|
||||||
|
*.ts text
|
||||||
|
*.tsx text
|
||||||
|
*.xml text
|
||||||
|
*.xhtml text
|
||||||
|
*.cs text diff=csharp
|
||||||
|
*.dockerignore text
|
||||||
|
Dockerfile text
|
||||||
|
|
||||||
|
|
||||||
|
# Template Type
|
||||||
|
*.dot text
|
||||||
|
*.ejs text
|
||||||
|
*.haml text
|
||||||
|
*.handlebars text
|
||||||
|
*.hbs text
|
||||||
|
*.hbt text
|
||||||
|
*.jade text
|
||||||
|
*.latte text
|
||||||
|
*.mustache text
|
||||||
|
*.njk text
|
||||||
|
*.phtml text
|
||||||
|
*.tmpl text
|
||||||
|
*.tpl text
|
||||||
|
*.twig text
|
||||||
|
|
||||||
|
# Visual Studio Setting
|
||||||
|
*.sln text eol=lf
|
||||||
|
*.csproj text eol=lf
|
||||||
|
*.vbproj text eol=lf
|
||||||
|
*.vcxproj text eol=lf
|
||||||
|
*.vcproj text eol=lf
|
||||||
|
*.dbproj text eol=lf
|
||||||
|
*.fsproj text eol=lf
|
||||||
|
*.lsproj text eol=lf
|
||||||
|
*.wixproj text eol=lf
|
||||||
|
*.modelproj text eol=lf
|
||||||
|
*.sqlproj text eol=lf
|
||||||
|
*.wmaproj text eol=lf
|
||||||
|
*.xproj text eol=lf
|
||||||
|
*.props text eol=lf
|
||||||
|
*.filters text eol=lf
|
||||||
|
*.vcxitems text eol=lf
|
||||||
|
|
||||||
|
# Linter
|
||||||
|
.csslintrc text
|
||||||
|
.eslintrc text
|
||||||
|
.jscsrc text
|
||||||
|
.jshintrc text
|
||||||
|
.jshintignore text
|
||||||
|
.stylelintrc text
|
||||||
|
.stylecop text
|
||||||
|
|
||||||
|
# Setting
|
||||||
|
*.bowerrc text
|
||||||
|
*.cnf text
|
||||||
|
*.conf text
|
||||||
|
*.config text
|
||||||
|
.browserslistrc text
|
||||||
|
.editorconfig text
|
||||||
|
.gitattributes text
|
||||||
|
.gitconfig text
|
||||||
|
.gitignore text
|
||||||
|
.htaccess text
|
||||||
|
*.npmignore text
|
||||||
|
*.yaml text
|
||||||
|
*.yml text
|
||||||
|
browserslist text
|
||||||
|
Makefile text
|
||||||
|
makefile text
|
||||||
|
*.ps1 text
|
||||||
|
|
||||||
|
## Binary
|
||||||
|
|
||||||
|
# Images
|
||||||
|
*.ai binary
|
||||||
|
*.bmp binary
|
||||||
|
*.eps binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.jng binary
|
||||||
|
*.jp2 binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.jpx binary
|
||||||
|
*.jxr binary
|
||||||
|
*.pdf binary
|
||||||
|
*.png binary
|
||||||
|
*.psb binary
|
||||||
|
*.psd binary
|
||||||
|
*.svg binary
|
||||||
|
*.svgz binary
|
||||||
|
*.tif binary
|
||||||
|
*.tiff binary
|
||||||
|
*.wbmp binary
|
||||||
|
*.webp binary
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
*.kar binary
|
||||||
|
*.m4a binary
|
||||||
|
*.mid binary
|
||||||
|
*.midi binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.ogg binary
|
||||||
|
*.ra binary
|
||||||
|
|
||||||
|
## Video
|
||||||
|
*.3gpp binary
|
||||||
|
*.3gp binary
|
||||||
|
*.as binary
|
||||||
|
*.asf binary
|
||||||
|
*.asx binary
|
||||||
|
*.fla binary
|
||||||
|
*.flv binary
|
||||||
|
*.m4v binary
|
||||||
|
*.mng binary
|
||||||
|
*.mov binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mpeg binary
|
||||||
|
*.mpg binary
|
||||||
|
*.swc binary
|
||||||
|
*.swf binary
|
||||||
|
*.webm binary
|
||||||
|
|
||||||
|
## Zip
|
||||||
|
*.7z binary
|
||||||
|
*.gz binary
|
||||||
|
*.rar binary
|
||||||
|
*.tar binary
|
||||||
|
*.zip binary
|
||||||
|
|
||||||
|
## FONTS
|
||||||
|
*.ttf binary
|
||||||
|
*.eot binary
|
||||||
|
*.otf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
|
||||||
|
## Exe
|
||||||
|
*.exe binary
|
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
/.eslintcache
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.0
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# BUILD STAGE
|
||||||
|
FROM node:14.14.0 as build
|
||||||
|
LABEL maintainer="Jason <jason@>"
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY [".", "./"]
|
||||||
|
ARG APP_ENV_ARG="dev"
|
||||||
|
ENV APP_ENV ${APP_ENV_ARG}
|
||||||
|
RUN npm run test:coverage && npm run build
|
||||||
|
|
||||||
|
# FINAL STAGE
|
||||||
|
FROM nginx:alpine
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
RUN chmod a+rwx /var/cache/nginx /var/run /var/log/nginx
|
||||||
|
RUN sed -i.bak 's/^user/#user/' /etc/nginx/nginx.conf
|
||||||
|
COPY --from=build /build/build/ /usr/share/nginx/html/
|
||||||
|
COPY ["./default.conf", "/etc/nginx/conf.d/default.conf"]
|
35
README.md
Normal file
35
README.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# keyCloak-Demo-Frontend
|
||||||
|
|
||||||
|
keyCloak Demo 用 Server
|
||||||
|
|
||||||
|
[![Coding Style](https://img.shields.io/badge/Coding%20Style-airbnb-blue)](https://github.com/airbnb/javascript)
|
||||||
|
|
||||||
|
## 🔧 Requirements
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/en/) 10.13+ with [npm](https://www.npmjs.com/)
|
||||||
|
- A fancy editor like [Vs Code](https://code.visualstudio.com/), [Sublime text](https://www.sublimetext.com/).
|
||||||
|
|
||||||
|
## 🚀 Project Quick Start
|
||||||
|
|
||||||
|
### Dev Server Guide
|
||||||
|
|
||||||
|
1. Clone the project from [keycloak-demo-frontend](https://git.trj.tw/keycloak-org/keycloak-demo-frontend.git).
|
||||||
|
2. Move the root path in project folder.
|
||||||
|
3. Run `npm i` or `npm install` to install node_modules.
|
||||||
|
4. The default server is on `localhost:4200`, please check you don't have any server on it.
|
||||||
|
5. Run `npm run start` to start dev server.
|
||||||
|
|
||||||
|
Steps
|
||||||
|
|
||||||
|
```git bash
|
||||||
|
git clone https://git.trj.tw/keycloak-org/keycloak-demo-frontend.git
|
||||||
|
cd keycloak-demo-frontend
|
||||||
|
npm install && npm audit fix
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📢 Information
|
||||||
|
|
||||||
|
- **Front End Framework** : [React](https://github.com/facebook/react)
|
||||||
|
- **CSS Library** : [Material UI React](https://github.com/mui-org/material-ui)
|
||||||
|
- **Coding Style** : [Airbnb](https://github.com/airbnb/javascript) (Use [Eslint](https://eslint.org/) to manager)
|
18
config/alias.js
Normal file
18
config/alias.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
'@Base': path.resolve(__dirname, '..', 'src', 'base'),
|
||||||
|
'@Components': path.resolve(__dirname, '..', 'src', 'components'),
|
||||||
|
'@Reducers': path.resolve(__dirname, '..', 'src', 'reducers'),
|
||||||
|
'@API': path.resolve(__dirname, '..', 'src', 'api'),
|
||||||
|
'@Tools': path.resolve(__dirname, '..', 'src', 'tools'),
|
||||||
|
'@Hooks': path.resolve(__dirname, '..', 'src', 'hooks'),
|
||||||
|
'@Models': path.resolve(__dirname, '..', 'src', 'models'),
|
||||||
|
'@CSS': path.resolve(__dirname, '..', 'src', 'css'),
|
||||||
|
'@Services': path.resolve(__dirname, '..', 'src', 'services'),
|
||||||
|
'@Providers': path.resolve(__dirname, '..', 'src', 'providers'),
|
||||||
|
'@Env': path.resolve(__dirname, '..', 'src', 'env'),
|
||||||
|
'@Langs': path.resolve(__dirname, '..', 'src', 'langs'),
|
||||||
|
'@Mocks': path.resolve(__dirname, '..', 'src', 'mocks'),
|
||||||
|
'@Plugin': path.resolve(__dirname, '..', 'src', 'plugin'),
|
||||||
|
};
|
108
config/env.js
Normal file
108
config/env.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const paths = require('./paths');
|
||||||
|
|
||||||
|
// Make sure that including paths.js after env.js will read .env variables.
|
||||||
|
delete require.cache[require.resolve('./paths')];
|
||||||
|
|
||||||
|
const NODE_ENV = process.env.NODE_ENV;
|
||||||
|
if (!NODE_ENV) {
|
||||||
|
throw new Error(
|
||||||
|
'The NODE_ENV environment variable is required but was not specified.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use
|
||||||
|
const dotenvFiles = [
|
||||||
|
`${paths.dotenv}.${NODE_ENV}.local`,
|
||||||
|
// Don't include `.env.local` for `test` environment
|
||||||
|
// since normally you expect tests to produce the same
|
||||||
|
// results for everyone
|
||||||
|
NODE_ENV !== 'test' && `${paths.dotenv}.local`,
|
||||||
|
`${paths.dotenv}.${NODE_ENV}`,
|
||||||
|
paths.dotenv,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
|
// Load environment variables from .env* files. Suppress warnings using silent
|
||||||
|
// if this file is missing. dotenv will never modify any environment variables
|
||||||
|
// that have already been set. Variable expansion is supported in .env files.
|
||||||
|
// https://github.com/motdotla/dotenv
|
||||||
|
// https://github.com/motdotla/dotenv-expand
|
||||||
|
dotenvFiles.forEach(dotenvFile => {
|
||||||
|
if (fs.existsSync(dotenvFile)) {
|
||||||
|
require('dotenv-expand')(
|
||||||
|
require('dotenv').config({
|
||||||
|
path: dotenvFile,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// We support resolving modules according to `NODE_PATH`.
|
||||||
|
// This lets you use absolute paths in imports inside large monorepos:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/253.
|
||||||
|
// It works similar to `NODE_PATH` in Node itself:
|
||||||
|
// https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders
|
||||||
|
// Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored.
|
||||||
|
// Otherwise, we risk importing Node.js core modules into an app instead of webpack shims.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421
|
||||||
|
// We also resolve them to make sure all tools using them work consistently.
|
||||||
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
|
process.env.NODE_PATH = (process.env.NODE_PATH || '')
|
||||||
|
.split(path.delimiter)
|
||||||
|
.filter(folder => folder && !path.isAbsolute(folder))
|
||||||
|
.map(folder => path.resolve(appDirectory, folder))
|
||||||
|
.join(path.delimiter);
|
||||||
|
|
||||||
|
// Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
|
||||||
|
// injected into the application via DefinePlugin in webpack configuration.
|
||||||
|
const REACT_APP = /^REACT_APP_/i;
|
||||||
|
|
||||||
|
function getClientEnvironment(publicUrl) {
|
||||||
|
const raw = Object.keys(process.env)
|
||||||
|
.filter(key => REACT_APP.test(key))
|
||||||
|
.reduce(
|
||||||
|
(env, key) => {
|
||||||
|
env[key] = process.env[key];
|
||||||
|
return env;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Useful for determining whether we’re running in production mode.
|
||||||
|
// Most importantly, it switches React into the correct mode.
|
||||||
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||||
|
// Useful for resolving the correct path to static assets in `public`.
|
||||||
|
// For example, <img src={process.env.PUBLIC_URL + '/img/logo.png'} />.
|
||||||
|
// This should only be used as an escape hatch. Normally you would put
|
||||||
|
// images into the `src` and `import` them in code to get their paths.
|
||||||
|
PUBLIC_URL: publicUrl,
|
||||||
|
// We support configuring the sockjs pathname during development.
|
||||||
|
// These settings let a developer run multiple simultaneous projects.
|
||||||
|
// They are used as the connection `hostname`, `pathname` and `port`
|
||||||
|
// in webpackHotDevClient. They are used as the `sockHost`, `sockPath`
|
||||||
|
// and `sockPort` options in webpack-dev-server.
|
||||||
|
WDS_SOCKET_HOST: process.env.WDS_SOCKET_HOST,
|
||||||
|
WDS_SOCKET_PATH: process.env.WDS_SOCKET_PATH,
|
||||||
|
WDS_SOCKET_PORT: process.env.WDS_SOCKET_PORT,
|
||||||
|
// Whether or not react-refresh is enabled.
|
||||||
|
// react-refresh is not 100% stable at this time,
|
||||||
|
// which is why it's disabled by default.
|
||||||
|
// It is defined here so it is available in the webpackHotDevClient.
|
||||||
|
FAST_REFRESH: process.env.FAST_REFRESH !== 'false',
|
||||||
|
APP_ENV: process.env.APP_ENV || 'development',
|
||||||
|
API_URL: process.env.API_URL || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// Stringify all values so we can feed into webpack DefinePlugin
|
||||||
|
const stringified = {
|
||||||
|
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||||
|
env[key] = JSON.stringify(raw[key]);
|
||||||
|
return env;
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return { raw, stringified };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getClientEnvironment;
|
66
config/getHttpsConfig.js
Normal file
66
config/getHttpsConfig.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const crypto = require('crypto');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const paths = require('./paths');
|
||||||
|
|
||||||
|
// Ensure the certificate and key provided are valid and if not
|
||||||
|
// throw an easy to debug error
|
||||||
|
function validateKeyAndCerts({ cert, key, keyFile, crtFile }) {
|
||||||
|
let encrypted;
|
||||||
|
try {
|
||||||
|
// publicEncrypt will throw an error with an invalid cert
|
||||||
|
encrypted = crypto.publicEncrypt(cert, Buffer.from('test'));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`The certificate "${chalk.yellow(crtFile)}" is invalid.\n${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// privateDecrypt will throw an error with an invalid key
|
||||||
|
crypto.privateDecrypt(key, encrypted);
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(
|
||||||
|
`The certificate key "${chalk.yellow(keyFile)}" is invalid.\n${
|
||||||
|
err.message
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file and throw an error if it doesn't exist
|
||||||
|
function readEnvFile(file, type) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error(
|
||||||
|
`You specified ${chalk.cyan(
|
||||||
|
type
|
||||||
|
)} in your env, but the file "${chalk.yellow(file)}" can't be found.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fs.readFileSync(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the https config
|
||||||
|
// Return cert files if provided in env, otherwise just true or false
|
||||||
|
function getHttpsConfig() {
|
||||||
|
const { SSL_CRT_FILE, SSL_KEY_FILE, HTTPS } = process.env;
|
||||||
|
const isHttps = HTTPS === 'true';
|
||||||
|
|
||||||
|
if (isHttps && SSL_CRT_FILE && SSL_KEY_FILE) {
|
||||||
|
const crtFile = path.resolve(paths.appPath, SSL_CRT_FILE);
|
||||||
|
const keyFile = path.resolve(paths.appPath, SSL_KEY_FILE);
|
||||||
|
const config = {
|
||||||
|
cert: readEnvFile(crtFile, 'SSL_CRT_FILE'),
|
||||||
|
key: readEnvFile(keyFile, 'SSL_KEY_FILE'),
|
||||||
|
};
|
||||||
|
|
||||||
|
validateKeyAndCerts({ ...config, keyFile, crtFile });
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
return isHttps;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getHttpsConfig;
|
14
config/jest/cssTransform.js
Normal file
14
config/jest/cssTransform.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// This is a custom Jest transformer turning style imports into empty objects.
|
||||||
|
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
process() {
|
||||||
|
return 'module.exports = {};';
|
||||||
|
},
|
||||||
|
getCacheKey() {
|
||||||
|
// The output is always the same.
|
||||||
|
return 'cssTransform';
|
||||||
|
},
|
||||||
|
};
|
40
config/jest/fileTransform.js
Normal file
40
config/jest/fileTransform.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const camelcase = require('camelcase');
|
||||||
|
|
||||||
|
// This is a custom Jest transformer turning file imports into filenames.
|
||||||
|
// http://facebook.github.io/jest/docs/en/webpack.html
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
process(src, filename) {
|
||||||
|
const assetFilename = JSON.stringify(path.basename(filename));
|
||||||
|
|
||||||
|
if (filename.match(/\.svg$/)) {
|
||||||
|
// Based on how SVGR generates a component name:
|
||||||
|
// https://github.com/smooth-code/svgr/blob/01b194cf967347d43d4cbe6b434404731b87cf27/packages/core/src/state.js#L6
|
||||||
|
const pascalCaseFilename = camelcase(path.parse(filename).name, {
|
||||||
|
pascalCase: true,
|
||||||
|
});
|
||||||
|
const componentName = `Svg${pascalCaseFilename}`;
|
||||||
|
return `const React = require('react');
|
||||||
|
module.exports = {
|
||||||
|
__esModule: true,
|
||||||
|
default: ${assetFilename},
|
||||||
|
ReactComponent: React.forwardRef(function ${componentName}(props, ref) {
|
||||||
|
return {
|
||||||
|
$$typeof: Symbol.for('react.element'),
|
||||||
|
type: 'svg',
|
||||||
|
ref: ref,
|
||||||
|
key: null,
|
||||||
|
props: Object.assign({}, props, {
|
||||||
|
children: ${assetFilename}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `module.exports = ${assetFilename};`;
|
||||||
|
},
|
||||||
|
};
|
134
config/modules.js
Normal file
134
config/modules.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const resolve = require('resolve');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get additional module paths based on the baseUrl of a compilerOptions object.
|
||||||
|
*
|
||||||
|
* @param {Object} options
|
||||||
|
*/
|
||||||
|
function getAdditionalModulePaths(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||||
|
|
||||||
|
// We don't need to do anything if `baseUrl` is set to `node_modules`. This is
|
||||||
|
// the default behavior.
|
||||||
|
if (path.relative(paths.appNodeModules, baseUrlResolved) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the user set the `baseUrl` to `appSrc`.
|
||||||
|
if (path.relative(paths.appSrc, baseUrlResolved) === '') {
|
||||||
|
return [paths.appSrc];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path is equal to the root directory we ignore it here.
|
||||||
|
// We don't want to allow importing from the root directly as source files are
|
||||||
|
// not transpiled outside of `src`. We do allow importing them with the
|
||||||
|
// absolute path (e.g. `src/Components/Button.js`) but we set that up with
|
||||||
|
// an alias.
|
||||||
|
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, throw an error.
|
||||||
|
throw new Error(
|
||||||
|
chalk.red.bold(
|
||||||
|
"Your project's `baseUrl` can only be set to `src` or `node_modules`." +
|
||||||
|
' Create React App does not support other values at this time.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get webpack aliases based on the baseUrl of a compilerOptions object.
|
||||||
|
*
|
||||||
|
* @param {*} options
|
||||||
|
*/
|
||||||
|
function getWebpackAliases(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||||
|
|
||||||
|
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||||
|
return {
|
||||||
|
src: paths.appSrc,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get jest aliases based on the baseUrl of a compilerOptions object.
|
||||||
|
*
|
||||||
|
* @param {*} options
|
||||||
|
*/
|
||||||
|
function getJestAliases(options = {}) {
|
||||||
|
const baseUrl = options.baseUrl;
|
||||||
|
|
||||||
|
if (!baseUrl) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrlResolved = path.resolve(paths.appPath, baseUrl);
|
||||||
|
|
||||||
|
if (path.relative(paths.appPath, baseUrlResolved) === '') {
|
||||||
|
return {
|
||||||
|
'^src/(.*)$': '<rootDir>/src/$1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getModules() {
|
||||||
|
// Check if TypeScript is setup
|
||||||
|
const hasTsConfig = fs.existsSync(paths.appTsConfig);
|
||||||
|
const hasJsConfig = fs.existsSync(paths.appJsConfig);
|
||||||
|
|
||||||
|
if (hasTsConfig && hasJsConfig) {
|
||||||
|
throw new Error(
|
||||||
|
'You have both a tsconfig.json and a jsconfig.json. If you are using TypeScript please remove your jsconfig.json file.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config;
|
||||||
|
|
||||||
|
// If there's a tsconfig.json we assume it's a
|
||||||
|
// TypeScript project and set up the config
|
||||||
|
// based on tsconfig.json
|
||||||
|
if (hasTsConfig) {
|
||||||
|
const ts = require(resolve.sync('typescript', {
|
||||||
|
basedir: paths.appNodeModules,
|
||||||
|
}));
|
||||||
|
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
|
||||||
|
// Otherwise we'll check if there is jsconfig.json
|
||||||
|
// for non TS projects.
|
||||||
|
} else if (hasJsConfig) {
|
||||||
|
config = require(paths.appJsConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
config = config || {};
|
||||||
|
const options = config.compilerOptions || {};
|
||||||
|
|
||||||
|
const additionalModulePaths = getAdditionalModulePaths(options);
|
||||||
|
|
||||||
|
return {
|
||||||
|
additionalModulePaths: additionalModulePaths,
|
||||||
|
webpackAliases: getWebpackAliases(options),
|
||||||
|
jestAliases: getJestAliases(options),
|
||||||
|
hasTsConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getModules();
|
73
config/paths.js
Normal file
73
config/paths.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath');
|
||||||
|
|
||||||
|
// Make sure any symlinks in the project folder are resolved:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/637
|
||||||
|
const appDirectory = fs.realpathSync(process.cwd());
|
||||||
|
const resolveApp = relativePath => path.resolve(appDirectory, relativePath);
|
||||||
|
|
||||||
|
// We use `PUBLIC_URL` environment variable or "homepage" field to infer
|
||||||
|
// "public path" at which the app is served.
|
||||||
|
// webpack needs to know it to put the right <script> hrefs into HTML even in
|
||||||
|
// single-page apps that may serve index.html for nested URLs like /todos/42.
|
||||||
|
// We can't use a relative path in HTML because we don't want to load something
|
||||||
|
// like /todos/42/static/js/bundle.7289d.js. We have to know the root.
|
||||||
|
const publicUrlOrPath = getPublicUrlOrPath(
|
||||||
|
process.env.NODE_ENV === 'development',
|
||||||
|
require(resolveApp('package.json')).homepage,
|
||||||
|
process.env.PUBLIC_URL
|
||||||
|
);
|
||||||
|
|
||||||
|
const moduleFileExtensions = [
|
||||||
|
'web.mjs',
|
||||||
|
'mjs',
|
||||||
|
'web.js',
|
||||||
|
'js',
|
||||||
|
'web.ts',
|
||||||
|
'ts',
|
||||||
|
'web.tsx',
|
||||||
|
'tsx',
|
||||||
|
'json',
|
||||||
|
'web.jsx',
|
||||||
|
'jsx',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Resolve file paths in the same order as webpack
|
||||||
|
const resolveModule = (resolveFn, filePath) => {
|
||||||
|
const extension = moduleFileExtensions.find(extension =>
|
||||||
|
fs.existsSync(resolveFn(`${filePath}.${extension}`))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (extension) {
|
||||||
|
return resolveFn(`${filePath}.${extension}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolveFn(`${filePath}.js`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// config after eject: we're in ./config/
|
||||||
|
module.exports = {
|
||||||
|
dotenv: resolveApp('.env'),
|
||||||
|
appPath: resolveApp('.'),
|
||||||
|
appBuild: resolveApp('build'),
|
||||||
|
appPublic: resolveApp('public'),
|
||||||
|
appHtml: resolveApp('public/index.html'),
|
||||||
|
appIndexJs: resolveModule(resolveApp, 'src/index'),
|
||||||
|
appPackageJson: resolveApp('package.json'),
|
||||||
|
appSrc: resolveApp('src'),
|
||||||
|
appTsConfig: resolveApp('tsconfig.json'),
|
||||||
|
appJsConfig: resolveApp('jsconfig.json'),
|
||||||
|
yarnLockFile: resolveApp('yarn.lock'),
|
||||||
|
testsSetup: resolveModule(resolveApp, 'src/setupTests'),
|
||||||
|
proxySetup: resolveApp('src/setupProxy.js'),
|
||||||
|
appNodeModules: resolveApp('node_modules'),
|
||||||
|
swSrc: resolveModule(resolveApp, 'src/service-worker'),
|
||||||
|
publicUrlOrPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports.moduleFileExtensions = moduleFileExtensions;
|
35
config/pnpTs.js
Normal file
35
config/pnpTs.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { resolveModuleName } = require('ts-pnp');
|
||||||
|
|
||||||
|
exports.resolveModuleName = (
|
||||||
|
typescript,
|
||||||
|
moduleName,
|
||||||
|
containingFile,
|
||||||
|
compilerOptions,
|
||||||
|
resolutionHost
|
||||||
|
) => {
|
||||||
|
return resolveModuleName(
|
||||||
|
moduleName,
|
||||||
|
containingFile,
|
||||||
|
compilerOptions,
|
||||||
|
resolutionHost,
|
||||||
|
typescript.resolveModuleName
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.resolveTypeReferenceDirective = (
|
||||||
|
typescript,
|
||||||
|
moduleName,
|
||||||
|
containingFile,
|
||||||
|
compilerOptions,
|
||||||
|
resolutionHost
|
||||||
|
) => {
|
||||||
|
return resolveModuleName(
|
||||||
|
moduleName,
|
||||||
|
containingFile,
|
||||||
|
compilerOptions,
|
||||||
|
resolutionHost,
|
||||||
|
typescript.resolveTypeReferenceDirective
|
||||||
|
);
|
||||||
|
};
|
532
config/webpack.config.js
Normal file
532
config/webpack.config.js
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const resolve = require('resolve');
|
||||||
|
const PnpWebpackPlugin = require('pnp-webpack-plugin');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
|
||||||
|
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
|
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||||
|
const safePostCssParser = require('postcss-safe-parser');
|
||||||
|
const ManifestPlugin = require('webpack-manifest-plugin');
|
||||||
|
const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin');
|
||||||
|
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');
|
||||||
|
const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
|
||||||
|
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
|
||||||
|
const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent');
|
||||||
|
const ESLintPlugin = require('eslint-webpack-plugin');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const modules = require('./modules');
|
||||||
|
const alias = require('./alias');
|
||||||
|
const getClientEnvironment = require('./env');
|
||||||
|
const ModuleNotFoundPlugin = require('react-dev-utils/ModuleNotFoundPlugin');
|
||||||
|
const ForkTsCheckerWebpackPlugin = require('react-dev-utils/ForkTsCheckerWebpackPlugin');
|
||||||
|
const typescriptFormatter = require('react-dev-utils/typescriptFormatter');
|
||||||
|
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
|
||||||
|
|
||||||
|
const postcssNormalize = require('postcss-normalize');
|
||||||
|
|
||||||
|
const appPackageJson = require(paths.appPackageJson);
|
||||||
|
|
||||||
|
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false';
|
||||||
|
|
||||||
|
const webpackDevClientEntry = require.resolve(
|
||||||
|
'react-dev-utils/webpackHotDevClient'
|
||||||
|
);
|
||||||
|
const reactRefreshOverlayEntry = require.resolve(
|
||||||
|
'react-dev-utils/refreshOverlayInterop'
|
||||||
|
);
|
||||||
|
|
||||||
|
const shouldInlineRuntimeChunk = process.env.INLINE_RUNTIME_CHUNK !== 'false';
|
||||||
|
|
||||||
|
const imageInlineSizeLimit = parseInt(
|
||||||
|
process.env.IMAGE_INLINE_SIZE_LIMIT || '10000'
|
||||||
|
);
|
||||||
|
|
||||||
|
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||||
|
|
||||||
|
const swSrc = paths.swSrc;
|
||||||
|
|
||||||
|
const cssRegex = /\.css$/;
|
||||||
|
const cssModuleRegex = /\.module\.css$/;
|
||||||
|
const sassRegex = /\.(scss|sass)$/;
|
||||||
|
const sassModuleRegex = /\.module\.(scss|sass)$/;
|
||||||
|
|
||||||
|
const hasJsxRuntime = (() => {
|
||||||
|
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
require.resolve('react/jsx-runtime');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
module.exports = function (webpackEnv) {
|
||||||
|
const isEnvDevelopment = webpackEnv === 'development';
|
||||||
|
const isEnvProduction = webpackEnv === 'production';
|
||||||
|
|
||||||
|
const isEnvProductionProfile =
|
||||||
|
isEnvProduction && process.argv.includes('--profile');
|
||||||
|
|
||||||
|
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||||
|
|
||||||
|
const shouldUseReactRefresh = env.raw.FAST_REFRESH;
|
||||||
|
|
||||||
|
const getStyleLoaders = (cssOptions, preProcessor) => {
|
||||||
|
const loaders = [
|
||||||
|
isEnvDevelopment && require.resolve('style-loader'),
|
||||||
|
isEnvProduction && {
|
||||||
|
loader: require.resolve('style-loader'),
|
||||||
|
options: paths.publicUrlOrPath.startsWith('.')
|
||||||
|
? { publicPath: '../../', attributes: { nonce: 'MzA3MWM2MTA4NjFhYzY1Y2RlOTRjZjdiODdkNzczNjkK'} }
|
||||||
|
: { attributes: { nonce: 'MzA3MWM2MTA4NjFhYzY1Y2RlOTRjZjdiODdkNzczNjkK'} },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve('css-loader'),
|
||||||
|
options: cssOptions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve('postcss-loader'),
|
||||||
|
options: {
|
||||||
|
ident: 'postcss',
|
||||||
|
plugins: () => [
|
||||||
|
require('postcss-flexbugs-fixes'),
|
||||||
|
require('postcss-preset-env')({
|
||||||
|
autoprefixer: {
|
||||||
|
flexbox: 'no-2009',
|
||||||
|
},
|
||||||
|
stage: 3,
|
||||||
|
}),
|
||||||
|
postcssNormalize(),
|
||||||
|
],
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
].filter(Boolean);
|
||||||
|
if (preProcessor) {
|
||||||
|
loaders.push(
|
||||||
|
{
|
||||||
|
loader: require.resolve('resolve-url-loader'),
|
||||||
|
options: {
|
||||||
|
sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
|
||||||
|
root: paths.appSrc,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve(preProcessor),
|
||||||
|
options: {
|
||||||
|
sourceMap: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return loaders;
|
||||||
|
};
|
||||||
|
console.log('-----------------');
|
||||||
|
console.log('Build Time Env ::: ', process.env.APP_ENV);
|
||||||
|
console.log('Build Time API Url ::: ', process.env.API_URL);
|
||||||
|
console.log('Build Time NODE Env ::: ', process.env.NODE_ENV);
|
||||||
|
console.log('Build Time Replace Origin Logo ::: ', process.env.REPLACE_COMPANY_LOGO);
|
||||||
|
console.log('-----------------');
|
||||||
|
return {
|
||||||
|
mode: isEnvProduction ? 'production' : isEnvDevelopment && 'development',
|
||||||
|
bail: isEnvProduction,
|
||||||
|
devtool: isEnvProduction
|
||||||
|
? shouldUseSourceMap
|
||||||
|
? 'source-map'
|
||||||
|
: false
|
||||||
|
: isEnvDevelopment && 'cheap-module-source-map',
|
||||||
|
entry:
|
||||||
|
isEnvDevelopment && !shouldUseReactRefresh
|
||||||
|
? [
|
||||||
|
webpackDevClientEntry,
|
||||||
|
paths.appIndexJs,
|
||||||
|
]
|
||||||
|
: paths.appIndexJs,
|
||||||
|
output: {
|
||||||
|
path: isEnvProduction ? paths.appBuild : undefined,
|
||||||
|
pathinfo: isEnvDevelopment,
|
||||||
|
filename: isEnvProduction
|
||||||
|
? 'static/js/[name].[contenthash:8].js'
|
||||||
|
: isEnvDevelopment && 'static/js/bundle.js',
|
||||||
|
futureEmitAssets: true,
|
||||||
|
chunkFilename: isEnvProduction
|
||||||
|
? 'static/js/[name].[contenthash:8].chunk.js'
|
||||||
|
: isEnvDevelopment && 'static/js/[name].chunk.js',
|
||||||
|
publicPath: paths.publicUrlOrPath,
|
||||||
|
devtoolModuleFilenameTemplate: isEnvProduction
|
||||||
|
? info =>
|
||||||
|
path
|
||||||
|
.relative(paths.appSrc, info.absoluteResourcePath)
|
||||||
|
.replace(/\\/g, '/')
|
||||||
|
: isEnvDevelopment &&
|
||||||
|
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
|
||||||
|
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
|
||||||
|
globalObject: 'this',
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isEnvProduction,
|
||||||
|
minimizer: [
|
||||||
|
new TerserPlugin({
|
||||||
|
terserOptions: {
|
||||||
|
parse: {
|
||||||
|
ecma: 8,
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
ecma: 5,
|
||||||
|
warnings: false,
|
||||||
|
comparisons: false,
|
||||||
|
inline: 2,
|
||||||
|
},
|
||||||
|
mangle: {
|
||||||
|
safari10: true,
|
||||||
|
},
|
||||||
|
keep_classnames: isEnvProductionProfile,
|
||||||
|
keep_fnames: isEnvProductionProfile,
|
||||||
|
output: {
|
||||||
|
ecma: 5,
|
||||||
|
comments: false,
|
||||||
|
ascii_only: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sourceMap: shouldUseSourceMap,
|
||||||
|
}),
|
||||||
|
new OptimizeCSSAssetsPlugin({
|
||||||
|
cssProcessorOptions: {
|
||||||
|
parser: safePostCssParser,
|
||||||
|
map: shouldUseSourceMap
|
||||||
|
? {
|
||||||
|
inline: false,
|
||||||
|
annotation: true,
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
},
|
||||||
|
cssProcessorPluginOptions: {
|
||||||
|
preset: ['default', { minifyFontValues: { removeQuotes: false } }],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'all',
|
||||||
|
name: false,
|
||||||
|
},
|
||||||
|
runtimeChunk: {
|
||||||
|
name: entrypoint => `runtime-${entrypoint.name}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: ['node_modules', paths.appNodeModules].concat(
|
||||||
|
modules.additionalModulePaths || []
|
||||||
|
),
|
||||||
|
extensions: paths.moduleFileExtensions
|
||||||
|
.map(ext => `.${ext}`)
|
||||||
|
.filter(ext => useTypeScript || !ext.includes('ts')),
|
||||||
|
alias: {
|
||||||
|
'react-native': 'react-native-web',
|
||||||
|
...(isEnvProductionProfile && {
|
||||||
|
'react-dom$': 'react-dom/profiling',
|
||||||
|
'scheduler/tracing': 'scheduler/tracing-profiling',
|
||||||
|
}),
|
||||||
|
...(modules.webpackAliases || {}),
|
||||||
|
...alias,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
PnpWebpackPlugin,
|
||||||
|
new ModuleScopePlugin(paths.appSrc, [
|
||||||
|
paths.appPackageJson,
|
||||||
|
reactRefreshOverlayEntry,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolveLoader: {
|
||||||
|
plugins: [
|
||||||
|
PnpWebpackPlugin.moduleLoader(module),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
strictExportPresence: true,
|
||||||
|
rules: [
|
||||||
|
{ parser: { requireEnsure: false } },
|
||||||
|
{
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
test: [/\.avif$/],
|
||||||
|
loader: require.resolve('url-loader'),
|
||||||
|
options: {
|
||||||
|
limit: imageInlineSizeLimit,
|
||||||
|
mimetype: 'image/avif',
|
||||||
|
name: 'static/media/[name].[hash:8].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
|
||||||
|
loader: require.resolve('url-loader'),
|
||||||
|
options: {
|
||||||
|
limit: imageInlineSizeLimit,
|
||||||
|
name: 'static/media/[name].[hash:8].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs|jsx|ts|tsx)$/,
|
||||||
|
include: paths.appSrc,
|
||||||
|
loader: require.resolve('babel-loader'),
|
||||||
|
options: {
|
||||||
|
customize: require.resolve(
|
||||||
|
'babel-preset-react-app/webpack-overrides'
|
||||||
|
),
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
require.resolve('babel-preset-react-app'),
|
||||||
|
{
|
||||||
|
runtime: hasJsxRuntime ? 'automatic' : 'classic',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
require.resolve('babel-plugin-named-asset-import'),
|
||||||
|
{
|
||||||
|
loaderMap: {
|
||||||
|
svg: {
|
||||||
|
ReactComponent:
|
||||||
|
'@svgr/webpack?-svgo,+titleProp,+ref![path]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isEnvDevelopment &&
|
||||||
|
shouldUseReactRefresh &&
|
||||||
|
require.resolve('react-refresh/babel'),
|
||||||
|
].filter(Boolean),
|
||||||
|
cacheDirectory: true,
|
||||||
|
cacheCompression: false,
|
||||||
|
compact: isEnvProduction,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.(js|mjs)$/,
|
||||||
|
exclude: /@babel(?:\/|\\{1,2})runtime/,
|
||||||
|
loader: require.resolve('babel-loader'),
|
||||||
|
options: {
|
||||||
|
babelrc: false,
|
||||||
|
configFile: false,
|
||||||
|
compact: false,
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
require.resolve('babel-preset-react-app/dependencies'),
|
||||||
|
{
|
||||||
|
helpers: true,
|
||||||
|
useBuiltIns: 'entry'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
cacheDirectory: true,
|
||||||
|
cacheCompression: false,
|
||||||
|
sourceMaps: shouldUseSourceMap,
|
||||||
|
inputSourceMap: shouldUseSourceMap,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: cssRegex,
|
||||||
|
exclude: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction
|
||||||
|
? shouldUseSourceMap
|
||||||
|
: isEnvDevelopment,
|
||||||
|
}),
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: cssModuleRegex,
|
||||||
|
use: getStyleLoaders({
|
||||||
|
importLoaders: 1,
|
||||||
|
sourceMap: isEnvProduction
|
||||||
|
? shouldUseSourceMap
|
||||||
|
: isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: sassRegex,
|
||||||
|
exclude: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 3,
|
||||||
|
sourceMap: isEnvProduction
|
||||||
|
? shouldUseSourceMap
|
||||||
|
: isEnvDevelopment,
|
||||||
|
},
|
||||||
|
'sass-loader'
|
||||||
|
),
|
||||||
|
sideEffects: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: sassModuleRegex,
|
||||||
|
use: getStyleLoaders(
|
||||||
|
{
|
||||||
|
importLoaders: 3,
|
||||||
|
sourceMap: isEnvProduction
|
||||||
|
? shouldUseSourceMap
|
||||||
|
: isEnvDevelopment,
|
||||||
|
modules: {
|
||||||
|
getLocalIdent: getCSSModuleLocalIdent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'sass-loader'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: require.resolve('file-loader'),
|
||||||
|
exclude: [/\.(js|mjs|jsx|ts|tsx)$/, /\.html$/, /\.json$/],
|
||||||
|
options: {
|
||||||
|
name: 'static/media/[name].[hash:8].[ext]',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin(
|
||||||
|
Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
inject: true,
|
||||||
|
template: paths.appHtml,
|
||||||
|
},
|
||||||
|
isEnvProduction
|
||||||
|
? {
|
||||||
|
minify: {
|
||||||
|
removeComments: true,
|
||||||
|
collapseWhitespace: true,
|
||||||
|
removeRedundantAttributes: true,
|
||||||
|
useShortDoctype: true,
|
||||||
|
removeEmptyAttributes: true,
|
||||||
|
removeStyleLinkTypeAttributes: true,
|
||||||
|
keepClosingSlash: true,
|
||||||
|
minifyJS: true,
|
||||||
|
minifyCSS: true,
|
||||||
|
minifyURLs: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
),
|
||||||
|
isEnvProduction &&
|
||||||
|
shouldInlineRuntimeChunk &&
|
||||||
|
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/runtime-.+[.]js/]),
|
||||||
|
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
|
||||||
|
new ModuleNotFoundPlugin(paths.appPath),
|
||||||
|
new webpack.DefinePlugin(env.stringified),
|
||||||
|
isEnvDevelopment && new webpack.HotModuleReplacementPlugin(),
|
||||||
|
isEnvDevelopment &&
|
||||||
|
shouldUseReactRefresh &&
|
||||||
|
new ReactRefreshWebpackPlugin({
|
||||||
|
overlay: {
|
||||||
|
entry: webpackDevClientEntry,
|
||||||
|
module: reactRefreshOverlayEntry,
|
||||||
|
sockIntegration: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
isEnvDevelopment && new CaseSensitivePathsPlugin(),
|
||||||
|
isEnvDevelopment &&
|
||||||
|
new WatchMissingNodeModulesPlugin(paths.appNodeModules),
|
||||||
|
isEnvProduction &&
|
||||||
|
new MiniCssExtractPlugin({
|
||||||
|
filename: 'static/css/[name].[contenthash:8].css',
|
||||||
|
chunkFilename: 'static/css/[name].[contenthash:8].chunk.css',
|
||||||
|
}),
|
||||||
|
new ManifestPlugin({
|
||||||
|
fileName: 'asset-manifest.json',
|
||||||
|
publicPath: paths.publicUrlOrPath,
|
||||||
|
generate: (seed, files, entrypoints) => {
|
||||||
|
const manifestFiles = files.reduce((manifest, file) => {
|
||||||
|
manifest[file.name] = file.path;
|
||||||
|
return manifest;
|
||||||
|
}, seed);
|
||||||
|
const entrypointFiles = entrypoints.main.filter(
|
||||||
|
fileName => !fileName.endsWith('.map')
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
files: manifestFiles,
|
||||||
|
entrypoints: entrypointFiles,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
isEnvProduction &&
|
||||||
|
fs.existsSync(swSrc) &&
|
||||||
|
new WorkboxWebpackPlugin.InjectManifest({
|
||||||
|
swSrc,
|
||||||
|
dontCacheBustURLsMatching: /\.[0-9a-f]{8}\./,
|
||||||
|
exclude: [/\.map$/, /asset-manifest\.json$/, /LICENSE/],
|
||||||
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||||
|
}),
|
||||||
|
useTypeScript &&
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
typescript: resolve.sync('typescript', {
|
||||||
|
basedir: paths.appNodeModules,
|
||||||
|
}),
|
||||||
|
async: isEnvDevelopment,
|
||||||
|
checkSyntacticErrors: true,
|
||||||
|
resolveModuleNameModule: process.versions.pnp
|
||||||
|
? `${__dirname}/pnpTs.js`
|
||||||
|
: undefined,
|
||||||
|
resolveTypeReferenceDirectiveModule: process.versions.pnp
|
||||||
|
? `${__dirname}/pnpTs.js`
|
||||||
|
: undefined,
|
||||||
|
tsconfig: paths.appTsConfig,
|
||||||
|
reportFiles: [
|
||||||
|
'../**/src/**/*.{ts,tsx}',
|
||||||
|
'**/src/**/*.{ts,tsx}',
|
||||||
|
'!**/src/**/__tests__/**',
|
||||||
|
'!**/src/**/?(*.)(spec|test).*',
|
||||||
|
'!**/src/setupProxy.*',
|
||||||
|
'!**/src/setupTests.*',
|
||||||
|
],
|
||||||
|
silent: true,
|
||||||
|
formatter: isEnvProduction ? typescriptFormatter : undefined,
|
||||||
|
}),
|
||||||
|
new ESLintPlugin({
|
||||||
|
extensions: ['js', 'mjs', 'jsx', 'ts', 'tsx'],
|
||||||
|
formatter: require.resolve('react-dev-utils/eslintFormatter'),
|
||||||
|
eslintPath: require.resolve('eslint'),
|
||||||
|
context: paths.appSrc,
|
||||||
|
cache: true,
|
||||||
|
cwd: paths.appPath,
|
||||||
|
resolvePluginsRelativeTo: __dirname,
|
||||||
|
baseConfig: {
|
||||||
|
extends: [require.resolve('eslint-config-react-app/base')],
|
||||||
|
rules: {
|
||||||
|
...(!hasJsxRuntime && {
|
||||||
|
'react/react-in-jsx-scope': 'error',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
].filter(Boolean),
|
||||||
|
node: {
|
||||||
|
module: 'empty',
|
||||||
|
dgram: 'empty',
|
||||||
|
dns: 'mock',
|
||||||
|
fs: 'empty',
|
||||||
|
http2: 'empty',
|
||||||
|
net: 'empty',
|
||||||
|
tls: 'empty',
|
||||||
|
child_process: 'empty',
|
||||||
|
},
|
||||||
|
performance: false,
|
||||||
|
};
|
||||||
|
};
|
130
config/webpackDevServer.config.js
Normal file
130
config/webpackDevServer.config.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const errorOverlayMiddleware = require('react-dev-utils/errorOverlayMiddleware');
|
||||||
|
const evalSourceMapMiddleware = require('react-dev-utils/evalSourceMapMiddleware');
|
||||||
|
const noopServiceWorkerMiddleware = require('react-dev-utils/noopServiceWorkerMiddleware');
|
||||||
|
const ignoredFiles = require('react-dev-utils/ignoredFiles');
|
||||||
|
const redirectServedPath = require('react-dev-utils/redirectServedPathMiddleware');
|
||||||
|
const paths = require('./paths');
|
||||||
|
const getHttpsConfig = require('./getHttpsConfig');
|
||||||
|
|
||||||
|
const host = process.env.HOST || '0.0.0.0';
|
||||||
|
const sockHost = process.env.WDS_SOCKET_HOST;
|
||||||
|
const sockPath = process.env.WDS_SOCKET_PATH; // default: '/sockjs-node'
|
||||||
|
const sockPort = process.env.WDS_SOCKET_PORT;
|
||||||
|
|
||||||
|
module.exports = function (proxy, allowedHost) {
|
||||||
|
return {
|
||||||
|
// WebpackDevServer 2.4.3 introduced a security fix that prevents remote
|
||||||
|
// websites from potentially accessing local content through DNS rebinding:
|
||||||
|
// https://github.com/webpack/webpack-dev-server/issues/887
|
||||||
|
// https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
|
||||||
|
// However, it made several existing use cases such as development in cloud
|
||||||
|
// environment or subdomains in development significantly more complicated:
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2271
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2233
|
||||||
|
// While we're investigating better solutions, for now we will take a
|
||||||
|
// compromise. Since our WDS configuration only serves files in the `public`
|
||||||
|
// folder we won't consider accessing them a vulnerability. However, if you
|
||||||
|
// use the `proxy` feature, it gets more dangerous because it can expose
|
||||||
|
// remote code execution vulnerabilities in backends like Django and Rails.
|
||||||
|
// So we will disable the host check normally, but enable it if you have
|
||||||
|
// specified the `proxy` setting. Finally, we let you override it if you
|
||||||
|
// really know what you're doing with a special environment variable.
|
||||||
|
disableHostCheck:
|
||||||
|
!proxy || process.env.DANGEROUSLY_DISABLE_HOST_CHECK === 'true',
|
||||||
|
// Enable gzip compression of generated files.
|
||||||
|
compress: true,
|
||||||
|
// Silence WebpackDevServer's own logs since they're generally not useful.
|
||||||
|
// It will still show compile warnings and errors with this setting.
|
||||||
|
clientLogLevel: 'none',
|
||||||
|
// By default WebpackDevServer serves physical files from current directory
|
||||||
|
// in addition to all the virtual build products that it serves from memory.
|
||||||
|
// This is confusing because those files won’t automatically be available in
|
||||||
|
// production build folder unless we copy them. However, copying the whole
|
||||||
|
// project directory is dangerous because we may expose sensitive files.
|
||||||
|
// Instead, we establish a convention that only files in `public` directory
|
||||||
|
// get served. Our build script will copy `public` into the `build` folder.
|
||||||
|
// In `index.html`, you can get URL of `public` folder with %PUBLIC_URL%:
|
||||||
|
// <link rel="icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
|
// In JavaScript code, you can access it with `process.env.PUBLIC_URL`.
|
||||||
|
// Note that we only recommend to use `public` folder as an escape hatch
|
||||||
|
// for files like `favicon.ico`, `manifest.json`, and libraries that are
|
||||||
|
// for some reason broken when imported through webpack. If you just want to
|
||||||
|
// use an image, put it in `src` and `import` it from JavaScript instead.
|
||||||
|
contentBase: paths.appPublic,
|
||||||
|
contentBasePublicPath: paths.publicUrlOrPath,
|
||||||
|
// By default files from `contentBase` will not trigger a page reload.
|
||||||
|
watchContentBase: true,
|
||||||
|
// Enable hot reloading server. It will provide WDS_SOCKET_PATH endpoint
|
||||||
|
// for the WebpackDevServer client so it can learn when the files were
|
||||||
|
// updated. The WebpackDevServer client is included as an entry point
|
||||||
|
// in the webpack development configuration. Note that only changes
|
||||||
|
// to CSS are currently hot reloaded. JS changes will refresh the browser.
|
||||||
|
hot: true,
|
||||||
|
// Use 'ws' instead of 'sockjs-node' on server since we're using native
|
||||||
|
// websockets in `webpackHotDevClient`.
|
||||||
|
transportMode: 'ws',
|
||||||
|
// Prevent a WS client from getting injected as we're already including
|
||||||
|
// `webpackHotDevClient`.
|
||||||
|
injectClient: false,
|
||||||
|
// Enable custom sockjs pathname for websocket connection to hot reloading server.
|
||||||
|
// Enable custom sockjs hostname, pathname and port for websocket connection
|
||||||
|
// to hot reloading server.
|
||||||
|
sockHost,
|
||||||
|
sockPath,
|
||||||
|
sockPort,
|
||||||
|
// It is important to tell WebpackDevServer to use the same "publicPath" path as
|
||||||
|
// we specified in the webpack config. When homepage is '.', default to serving
|
||||||
|
// from the root.
|
||||||
|
// remove last slash so user can land on `/test` instead of `/test/`
|
||||||
|
publicPath: paths.publicUrlOrPath.slice(0, -1),
|
||||||
|
// WebpackDevServer is noisy by default so we emit custom message instead
|
||||||
|
// by listening to the compiler events with `compiler.hooks[...].tap` calls above.
|
||||||
|
quiet: true,
|
||||||
|
// Reportedly, this avoids CPU overload on some systems.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/293
|
||||||
|
// src/node_modules is not ignored to support absolute imports
|
||||||
|
// https://github.com/facebook/create-react-app/issues/1065
|
||||||
|
watchOptions: {
|
||||||
|
ignored: ignoredFiles(paths.appSrc),
|
||||||
|
},
|
||||||
|
https: getHttpsConfig(),
|
||||||
|
host,
|
||||||
|
overlay: false,
|
||||||
|
historyApiFallback: {
|
||||||
|
// Paths with dots should still use the history fallback.
|
||||||
|
// See https://github.com/facebook/create-react-app/issues/387.
|
||||||
|
disableDotRule: true,
|
||||||
|
index: paths.publicUrlOrPath,
|
||||||
|
},
|
||||||
|
public: allowedHost,
|
||||||
|
// `proxy` is run between `before` and `after` `webpack-dev-server` hooks
|
||||||
|
proxy,
|
||||||
|
before(app, server) {
|
||||||
|
// Keep `evalSourceMapMiddleware` and `errorOverlayMiddleware`
|
||||||
|
// middlewares before `redirectServedPath` otherwise will not have any effect
|
||||||
|
// This lets us fetch source contents from webpack for the error overlay
|
||||||
|
app.use(evalSourceMapMiddleware(server));
|
||||||
|
// This lets us open files from the runtime error overlay.
|
||||||
|
app.use(errorOverlayMiddleware());
|
||||||
|
|
||||||
|
if (fs.existsSync(paths.proxySetup)) {
|
||||||
|
// This registers user provided middleware for proxy reasons
|
||||||
|
require(paths.proxySetup)(app);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
after(app) {
|
||||||
|
// Redirect to `PUBLIC_URL` or `homepage` from `package.json` if url not match
|
||||||
|
app.use(redirectServedPath(paths.publicUrlOrPath));
|
||||||
|
|
||||||
|
// This service worker file is effectively a 'no-op' that will reset any
|
||||||
|
// previous service worker registered for the same host:port combination.
|
||||||
|
// We do this in development to avoid hitting the production cache if
|
||||||
|
// it used the same host and port.
|
||||||
|
// https://github.com/facebook/create-react-app/issues/2272#issuecomment-302832432
|
||||||
|
app.use(noopServiceWorkerMiddleware(paths.publicUrlOrPath));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
23
default.conf
Normal file
23
default.conf
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
port_in_redirect off;
|
||||||
|
absolute_redirect off;
|
||||||
|
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
location / {
|
||||||
|
#add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; base-uri 'self'";
|
||||||
|
add_header X-Frame-Options SAMEORIGIN;
|
||||||
|
add_header Cache-Control no-cache;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection '1; mode=block';
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
}
|
||||||
|
}
|
43401
package-lock.json
generated
Normal file
43401
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
219
package.json
Normal file
219
package.json
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
{
|
||||||
|
"name": "keycloak-demo-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core": "7.13.8",
|
||||||
|
"@date-io/dayjs": "^1.3.13",
|
||||||
|
"@material-ui/core": "^4.11.3",
|
||||||
|
"@material-ui/icons": "^4.11.2",
|
||||||
|
"@material-ui/lab": "^4.0.0-alpha.57",
|
||||||
|
"@material-ui/pickers": "^3.2.10",
|
||||||
|
"@types/jest": "^26.0.20",
|
||||||
|
"@types/node": "^12.19.9",
|
||||||
|
"@types/react": "^16.14.2",
|
||||||
|
"@types/react-dom": "^16.9.10",
|
||||||
|
"@types/react-redux": "^7.1.16",
|
||||||
|
"@types/react-router-dom": "^5.1.7",
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"classnames": "^2.2.6",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dayjs": "^1.10.4",
|
||||||
|
"immer": "^8.0.1",
|
||||||
|
"js-base64": "^3.6.0",
|
||||||
|
"jsonwebtoken": "^8.5.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"qs": "^6.5.2",
|
||||||
|
"react": "16.13.1",
|
||||||
|
"react-color": "^2.19.3",
|
||||||
|
"react-dom": "16.13.1",
|
||||||
|
"react-redux": "^7.2.2",
|
||||||
|
"react-refresh": "^0.8.3",
|
||||||
|
"react-router": "^5.2.0",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"redux": "^4.0.5",
|
||||||
|
"redux-thunk": "^2.3.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node scripts/start.js --progress",
|
||||||
|
"build": "npm run clean && node scripts/build.js --progress",
|
||||||
|
"test": "node scripts/test.js",
|
||||||
|
"test:simple": "npm run test -- --watchAll=false",
|
||||||
|
"test:coverage": "npm run test -- --coverage --watchAll=false",
|
||||||
|
"dev": "cross-env NODE_ENV=development APP_ENV=dev npm run start",
|
||||||
|
"stage": "cross-env NODE_ENV=development APP_ENV=stage npm run start",
|
||||||
|
"production": "cross-env NODE_ENV=production APP_ENV=production npm run start",
|
||||||
|
"renew:dev": "rm -rf node_modules && npm i && npm audit fix && npm run dev",
|
||||||
|
"renew:stage": "rm -rf node_modules && npm i && npm audit fix && npm run stage",
|
||||||
|
"renew:prod": "rm -rf node_modules && npm i && npm audit fix && npm run production",
|
||||||
|
"lint": "eslint --ext .js,.jsx,.ts,.tsx ./src",
|
||||||
|
"lintFix": "eslint --ext .js,.jsx,.ts,.tsx ./src --fix",
|
||||||
|
"clean": "rimraf ./build",
|
||||||
|
"createTemplate": "node scripts/TemplateTool/index.js"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all",
|
||||||
|
"ie 11"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version",
|
||||||
|
"ie 11"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"roots": [
|
||||||
|
"<rootDir>/src"
|
||||||
|
],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"!src/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"coveragePathIgnorePatterns": [
|
||||||
|
"node_modules"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"react-app-polyfill/jsdom"
|
||||||
|
],
|
||||||
|
"setupFilesAfterEnv": [
|
||||||
|
"<rootDir>/src/services/Scripts/setupTests.ts"
|
||||||
|
],
|
||||||
|
"testMatch": [
|
||||||
|
"<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}",
|
||||||
|
"<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"
|
||||||
|
],
|
||||||
|
"testEnvironment": "jsdom",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(js|jsx|mjs|cjs|ts|tsx)$": "<rootDir>/node_modules/babel-jest",
|
||||||
|
"^.+\\.css$": "<rootDir>/config/jest/cssTransform.js",
|
||||||
|
"^(?!.*\\.(js|jsx|mjs|cjs|ts|tsx|css|json)$)": "<rootDir>/config/jest/fileTransform.js"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs|cjs|ts|tsx)$",
|
||||||
|
"^.+\\.module\\.(css|sass|scss)$"
|
||||||
|
],
|
||||||
|
"modulePaths": [],
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^react-native$": "react-native-web",
|
||||||
|
"^.+\\.module\\.(css|sass|scss)$": "identity-obj-proxy",
|
||||||
|
"^@Base(.*)$": "<rootDir>/src/base/$1",
|
||||||
|
"^@Components(.*)$": "<rootDir>/src/components/$1",
|
||||||
|
"^@Reducers(.*)$": "<rootDir>/src/reducers/$1",
|
||||||
|
"^@API(.*)$": "<rootDir>/src/api/$1",
|
||||||
|
"^@Hooks(.*)$": "<rootDir>/src/hooks/$1",
|
||||||
|
"^@Models(.*)$": "<rootDir>/src/models/$1",
|
||||||
|
"^@CSS(.*)$": "<rootDir>/src/css/$1",
|
||||||
|
"^@Services(.*)$": "<rootDir>/src/services/$1",
|
||||||
|
"^@Providers(.*)$": "<rootDir>/src/providers/$1",
|
||||||
|
"^@Env(.*)$": "<rootDir>/src/env/$1",
|
||||||
|
"^@Langs(.*)$": "<rootDir>/src/langs/$1",
|
||||||
|
"^@Mocks(.*)$": "<rootDir>/src/mocks/$1",
|
||||||
|
"^@Tools(.*)$": "<rootDir>/src/tools/$1",
|
||||||
|
"^@Plugin(.*)$": "<rootDir>/src/plugin/$1"
|
||||||
|
},
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"web.js",
|
||||||
|
"js",
|
||||||
|
"web.ts",
|
||||||
|
"ts",
|
||||||
|
"web.tsx",
|
||||||
|
"tsx",
|
||||||
|
"json",
|
||||||
|
"web.jsx",
|
||||||
|
"jsx",
|
||||||
|
"node"
|
||||||
|
],
|
||||||
|
"watchPlugins": [
|
||||||
|
"jest-watch-typeahead/filename",
|
||||||
|
"jest-watch-typeahead/testname"
|
||||||
|
],
|
||||||
|
"resetMocks": true
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"react-app"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "0.4.3",
|
||||||
|
"@svgr/webpack": "5.5.0",
|
||||||
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
|
"@testing-library/react": "^11.2.5",
|
||||||
|
"@testing-library/user-event": "^12.8.1",
|
||||||
|
"@types/classnames": "^2.2.11",
|
||||||
|
"@types/lodash": "^4.14.168",
|
||||||
|
"@types/react-color": "^3.0.4",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^4.16.1",
|
||||||
|
"@typescript-eslint/parser": "^4.16.1",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"babel-jest": "^26.6.3",
|
||||||
|
"babel-loader": "8.2.2",
|
||||||
|
"babel-plugin-named-asset-import": "^0.3.7",
|
||||||
|
"babel-preset-react-app": "^10.0.0",
|
||||||
|
"bfj": "^7.0.2",
|
||||||
|
"camelcase": "^6.2.0",
|
||||||
|
"case-sensitive-paths-webpack-plugin": "2.4.0",
|
||||||
|
"css-loader": "4.3.0",
|
||||||
|
"dotenv": "8.2.0",
|
||||||
|
"dotenv-expand": "5.1.0",
|
||||||
|
"eslint": "^7.21.0",
|
||||||
|
"eslint-config-airbnb": "^18.2.1",
|
||||||
|
"eslint-config-prettier": "^7.1.0",
|
||||||
|
"eslint-config-react-app": "^6.0.0",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
|
"eslint-import-resolver-typescript": "^2.4.0",
|
||||||
|
"eslint-plugin-flowtype": "^5.3.1",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-jest": "^24.1.8",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||||
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
|
"eslint-plugin-react": "^7.22.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"eslint-plugin-testing-library": "^3.10.1",
|
||||||
|
"eslint-webpack-plugin": "^2.5.2",
|
||||||
|
"file-loader": "6.2.0",
|
||||||
|
"fs-extra": "^9.1.0",
|
||||||
|
"html-webpack-plugin": "4.5.0",
|
||||||
|
"identity-obj-proxy": "3.0.0",
|
||||||
|
"jest": "26.6.3",
|
||||||
|
"jest-circus": "26.6.3",
|
||||||
|
"jest-resolve": "26.6.2",
|
||||||
|
"jest-watch-typeahead": "0.6.1",
|
||||||
|
"mini-css-extract-plugin": "0.11.3",
|
||||||
|
"optimize-css-assets-webpack-plugin": "5.0.4",
|
||||||
|
"pnp-webpack-plugin": "1.6.4",
|
||||||
|
"postcss-flexbugs-fixes": "4.2.1",
|
||||||
|
"postcss-loader": "3.0.0",
|
||||||
|
"postcss-normalize": "8.0.1",
|
||||||
|
"postcss-preset-env": "6.7.0",
|
||||||
|
"postcss-safe-parser": "5.0.2",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"prompts": "2.4.0",
|
||||||
|
"react-app-polyfill": "^2.0.0",
|
||||||
|
"react-dev-utils": "^11.0.3",
|
||||||
|
"resolve": "1.20.0",
|
||||||
|
"resolve-url-loader": "^3.1.2",
|
||||||
|
"sass-loader": "8.0.2",
|
||||||
|
"semver": "7.3.4",
|
||||||
|
"style-loader": "1.3.0",
|
||||||
|
"terser-webpack-plugin": "4.2.3",
|
||||||
|
"ts-pnp": "1.2.0",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
|
"url-loader": "4.1.1",
|
||||||
|
"webpack": "4.44.2",
|
||||||
|
"webpack-dev-server": "3.11.2",
|
||||||
|
"webpack-manifest-plugin": "2.2.0",
|
||||||
|
"workbox-webpack-plugin": "5.1.4"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
20
public/index.html
Normal file
20
public/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" id="app">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/logo.png" />
|
||||||
|
<meta property="csp-nonce" content="MzA3MWM2MTA4NjFhYzY1Y2RlOTRjZjdiODdkNzczNjkK" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Web site created using create-react-app"
|
||||||
|
/>
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo.png" />
|
||||||
|
<title>KeyCloak Frontend Demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
212
scripts/build.js
Normal file
212
scripts/build.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'production';
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const bfj = require('bfj');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages');
|
||||||
|
const printHostingInstructions = require('react-dev-utils/printHostingInstructions');
|
||||||
|
const FileSizeReporter = require('react-dev-utils/FileSizeReporter');
|
||||||
|
const printBuildError = require('react-dev-utils/printBuildError');
|
||||||
|
|
||||||
|
const measureFileSizesBeforeBuild =
|
||||||
|
FileSizeReporter.measureFileSizesBeforeBuild;
|
||||||
|
const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild;
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
|
||||||
|
// These sizes are pretty large. We'll warn for bundles exceeding them.
|
||||||
|
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||||
|
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||||
|
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const argv = process.argv.slice(2);
|
||||||
|
const writeStatsJson = argv.indexOf('--stats') !== -1;
|
||||||
|
|
||||||
|
// Generate configuration
|
||||||
|
const config = configFactory('production');
|
||||||
|
|
||||||
|
// We require that you explicitly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// First, read the current file sizes in build directory.
|
||||||
|
// This lets us display how much they changed later.
|
||||||
|
return measureFileSizesBeforeBuild(paths.appBuild);
|
||||||
|
})
|
||||||
|
.then(previousFileSizes => {
|
||||||
|
// Remove all content but keep the directory so that
|
||||||
|
// if you're in it, you don't end up in Trash
|
||||||
|
fs.emptyDirSync(paths.appBuild);
|
||||||
|
// Merge with the public folder
|
||||||
|
copyPublicFolder();
|
||||||
|
// Start the webpack build
|
||||||
|
return build(previousFileSizes);
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
({ stats, previousFileSizes, warnings }) => {
|
||||||
|
if (warnings.length) {
|
||||||
|
console.log(chalk.yellow('Compiled with warnings.\n'));
|
||||||
|
console.log(warnings.join('\n\n'));
|
||||||
|
console.log(
|
||||||
|
'\nSearch for the ' +
|
||||||
|
chalk.underline(chalk.yellow('keywords')) +
|
||||||
|
' to learn more about each warning.'
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'To ignore, add ' +
|
||||||
|
chalk.cyan('// eslint-disable-next-line') +
|
||||||
|
' to the line before.\n'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.green('Compiled successfully.\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('File sizes after gzip:\n');
|
||||||
|
printFileSizesAfterBuild(
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
paths.appBuild,
|
||||||
|
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||||
|
WARN_AFTER_CHUNK_GZIP_SIZE
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
const appPackage = require(paths.appPackageJson);
|
||||||
|
const publicUrl = paths.publicUrlOrPath;
|
||||||
|
const publicPath = config.output.publicPath;
|
||||||
|
const buildFolder = path.relative(process.cwd(), paths.appBuild);
|
||||||
|
printHostingInstructions(
|
||||||
|
appPackage,
|
||||||
|
publicUrl,
|
||||||
|
publicPath,
|
||||||
|
buildFolder,
|
||||||
|
useYarn
|
||||||
|
);
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||||
|
if (tscCompileOnError) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'Compiled with the following type errors (you may want to check these before deploying your app):\n'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
printBuildError(err);
|
||||||
|
} else {
|
||||||
|
console.log(chalk.red('Failed to compile.\n'));
|
||||||
|
printBuildError(err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the production build and print the deployment instructions.
|
||||||
|
function build(previousFileSizes) {
|
||||||
|
console.log('Creating an optimized production build...');
|
||||||
|
|
||||||
|
const compiler = webpack(config);
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
compiler.run((err, stats) => {
|
||||||
|
let messages;
|
||||||
|
if (err) {
|
||||||
|
if (!err.message) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let errMessage = err.message;
|
||||||
|
|
||||||
|
// Add additional information for postcss errors
|
||||||
|
if (Object.prototype.hasOwnProperty.call(err, 'postcssNode')) {
|
||||||
|
errMessage +=
|
||||||
|
'\nCompileError: Begins at CSS selector ' +
|
||||||
|
err['postcssNode'].selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages = formatWebpackMessages({
|
||||||
|
errors: [errMessage],
|
||||||
|
warnings: [],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
messages = formatWebpackMessages(
|
||||||
|
stats.toJson({ all: false, warnings: true, errors: true })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (messages.errors.length) {
|
||||||
|
// Only keep the first error. Others are often indicative
|
||||||
|
// of the same problem, but confuse the reader with noise.
|
||||||
|
if (messages.errors.length > 1) {
|
||||||
|
messages.errors.length = 1;
|
||||||
|
}
|
||||||
|
return reject(new Error(messages.errors.join('\n\n')));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
process.env.CI &&
|
||||||
|
(typeof process.env.CI !== 'string' ||
|
||||||
|
process.env.CI.toLowerCase() !== 'false') &&
|
||||||
|
messages.warnings.length
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
'\nTreating warnings as errors because process.env.CI = true.\n' +
|
||||||
|
'Most CI servers set it automatically.\n'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return reject(new Error(messages.warnings.join('\n\n')));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveArgs = {
|
||||||
|
stats,
|
||||||
|
previousFileSizes,
|
||||||
|
warnings: messages.warnings,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (writeStatsJson) {
|
||||||
|
return bfj
|
||||||
|
.write(paths.appBuild + '/bundle-stats.json', stats.toJson())
|
||||||
|
.then(() => resolve(resolveArgs))
|
||||||
|
.catch(error => reject(new Error(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(resolveArgs);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPublicFolder() {
|
||||||
|
fs.copySync(paths.appPublic, paths.appBuild, {
|
||||||
|
dereference: true,
|
||||||
|
filter: file => file !== paths.appHtml,
|
||||||
|
});
|
||||||
|
}
|
166
scripts/start.js
Normal file
166
scripts/start.js
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'development';
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const chalk = require('react-dev-utils/chalk');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const WebpackDevServer = require('webpack-dev-server');
|
||||||
|
const clearConsole = require('react-dev-utils/clearConsole');
|
||||||
|
const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles');
|
||||||
|
const {
|
||||||
|
choosePort,
|
||||||
|
createCompiler,
|
||||||
|
prepareProxy,
|
||||||
|
prepareUrls,
|
||||||
|
} = require('react-dev-utils/WebpackDevServerUtils');
|
||||||
|
const openBrowser = require('react-dev-utils/openBrowser');
|
||||||
|
const semver = require('semver');
|
||||||
|
const paths = require('../config/paths');
|
||||||
|
const configFactory = require('../config/webpack.config');
|
||||||
|
const createDevServerConfig = require('../config/webpackDevServer.config');
|
||||||
|
const getClientEnvironment = require('../config/env');
|
||||||
|
const react = require(require.resolve('react', { paths: [paths.appPath] }));
|
||||||
|
|
||||||
|
const env = getClientEnvironment(paths.publicUrlOrPath.slice(0, -1));
|
||||||
|
const useYarn = fs.existsSync(paths.yarnLockFile);
|
||||||
|
const isInteractive = process.stdout.isTTY;
|
||||||
|
|
||||||
|
// Warn and crash if required files are missing
|
||||||
|
if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools like Cloud9 rely on this.
|
||||||
|
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 4200;
|
||||||
|
const HOST = process.env.HOST || '0.0.0.0';
|
||||||
|
|
||||||
|
if (process.env.HOST) {
|
||||||
|
console.log(
|
||||||
|
chalk.cyan(
|
||||||
|
`Attempting to bind to HOST environment variable: ${chalk.yellow(
|
||||||
|
chalk.bold(process.env.HOST)
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`If this was unintentional, check that you haven't mistakenly set it in your shell.`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Learn more here: ${chalk.yellow('https://cra.link/advanced-config')}`
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We require that you explicitly set browsers and do not fall back to
|
||||||
|
// browserslist defaults.
|
||||||
|
const { checkBrowsers } = require('react-dev-utils/browsersHelper');
|
||||||
|
checkBrowsers(paths.appPath, isInteractive)
|
||||||
|
.then(() => {
|
||||||
|
// We attempt to use the default port but if it is busy, we offer the user to
|
||||||
|
// run on a different port. `choosePort()` Promise resolves to the next free port.
|
||||||
|
return choosePort(HOST, DEFAULT_PORT);
|
||||||
|
})
|
||||||
|
.then(port => {
|
||||||
|
if (port == null) {
|
||||||
|
// We have not found a port.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = configFactory('development');
|
||||||
|
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||||
|
const appName = require(paths.appPackageJson).name;
|
||||||
|
|
||||||
|
const useTypeScript = fs.existsSync(paths.appTsConfig);
|
||||||
|
const tscCompileOnError = process.env.TSC_COMPILE_ON_ERROR === 'true';
|
||||||
|
const urls = prepareUrls(
|
||||||
|
protocol,
|
||||||
|
HOST,
|
||||||
|
port,
|
||||||
|
paths.publicUrlOrPath.slice(0, -1)
|
||||||
|
);
|
||||||
|
const devSocket = {
|
||||||
|
warnings: warnings =>
|
||||||
|
devServer.sockWrite(devServer.sockets, 'warnings', warnings),
|
||||||
|
errors: errors =>
|
||||||
|
devServer.sockWrite(devServer.sockets, 'errors', errors),
|
||||||
|
};
|
||||||
|
// Create a webpack compiler that is configured with custom messages.
|
||||||
|
const compiler = createCompiler({
|
||||||
|
appName,
|
||||||
|
config,
|
||||||
|
devSocket,
|
||||||
|
urls,
|
||||||
|
useYarn,
|
||||||
|
useTypeScript,
|
||||||
|
tscCompileOnError,
|
||||||
|
webpack,
|
||||||
|
});
|
||||||
|
// Load proxy config
|
||||||
|
const proxySetting = require(paths.appPackageJson).proxy;
|
||||||
|
const proxyConfig = prepareProxy(
|
||||||
|
proxySetting,
|
||||||
|
paths.appPublic,
|
||||||
|
paths.publicUrlOrPath
|
||||||
|
);
|
||||||
|
// Serve webpack assets generated by the compiler over a web server.
|
||||||
|
const serverConfig = createDevServerConfig(
|
||||||
|
proxyConfig,
|
||||||
|
urls.lanUrlForConfig
|
||||||
|
);
|
||||||
|
const devServer = new WebpackDevServer(compiler, serverConfig);
|
||||||
|
// Launch WebpackDevServer.
|
||||||
|
devServer.listen(port, HOST, err => {
|
||||||
|
if (err) {
|
||||||
|
return console.log(err);
|
||||||
|
}
|
||||||
|
if (isInteractive) {
|
||||||
|
clearConsole();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (env.raw.FAST_REFRESH && semver.lt(react.version, '16.10.0')) {
|
||||||
|
console.log(
|
||||||
|
chalk.yellow(
|
||||||
|
`Fast Refresh requires React 16.10 or higher. You are using React ${react.version}.`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(chalk.cyan('Starting the development server...\n'));
|
||||||
|
openBrowser(urls.localUrlForBrowser);
|
||||||
|
});
|
||||||
|
|
||||||
|
['SIGINT', 'SIGTERM'].forEach(function (sig) {
|
||||||
|
process.on(sig, function () {
|
||||||
|
devServer.close();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.CI !== 'true') {
|
||||||
|
// Gracefully exit when stdin ends
|
||||||
|
process.stdin.on('end', function () {
|
||||||
|
devServer.close();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err && err.message) {
|
||||||
|
console.log(err.message);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
53
scripts/test.js
Normal file
53
scripts/test.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Do this as the first thing so that any code reading it knows the right env.
|
||||||
|
process.env.BABEL_ENV = 'test';
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
process.env.PUBLIC_URL = '';
|
||||||
|
|
||||||
|
// Makes the script crash on unhandled rejections instead of silently
|
||||||
|
// ignoring them. In the future, promise rejections that are not handled will
|
||||||
|
// terminate the Node.js process with a non-zero exit code.
|
||||||
|
process.on('unhandledRejection', err => {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure environment variables are read.
|
||||||
|
require('../config/env');
|
||||||
|
|
||||||
|
|
||||||
|
const jest = require('jest');
|
||||||
|
const execSync = require('child_process').execSync;
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
function isInGitRepository() {
|
||||||
|
try {
|
||||||
|
execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInMercurialRepository() {
|
||||||
|
try {
|
||||||
|
execSync('hg --cwd . root', { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch unless on CI or explicitly running all tests
|
||||||
|
if (
|
||||||
|
!process.env.CI &&
|
||||||
|
argv.indexOf('--watchAll') === -1 &&
|
||||||
|
argv.indexOf('--watchAll=false') === -1
|
||||||
|
) {
|
||||||
|
// https://github.com/facebook/create-react-app/issues/5210
|
||||||
|
const hasSourceControl = isInGitRepository() || isInMercurialRepository();
|
||||||
|
argv.push(hasSourceControl ? '--watch' : '--watchAll');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
jest.run(argv);
|
24
src/api/_APITool/Header.ts
Normal file
24
src/api/_APITool/Header.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { HeaderContent } from './types';
|
||||||
|
|
||||||
|
class Header {
|
||||||
|
public header: HeaderContent;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.header = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
updateToken(token: string): void {
|
||||||
|
this.header = {
|
||||||
|
...this.header,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
removeToken(): void {
|
||||||
|
if ('Authorization' in this.header) {
|
||||||
|
delete this.header.Authorization;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Header;
|
5
src/api/_APITool/index.ts
Normal file
5
src/api/_APITool/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
|
export const createAxios = ({ host }: { host: string }): AxiosInstance => axios.create({
|
||||||
|
baseURL: host,
|
||||||
|
});
|
3
src/api/_APITool/types.d.ts
vendored
Normal file
3
src/api/_APITool/types.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface HeaderContent {
|
||||||
|
Authorization?: string;
|
||||||
|
}
|
49
src/api/index.ts
Normal file
49
src/api/index.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import { getIsJwtToken, getIsJwtTokenExpire } from '@Tools/utility';
|
||||||
|
import Header from './_APITool/Header';
|
||||||
|
import { createAxios } from './_APITool';
|
||||||
|
import { CreatUserAPI } from './user-apis';
|
||||||
|
import { UserAPIProps } from './user-apis/types';
|
||||||
|
import { APIParams } from './types';
|
||||||
|
|
||||||
|
class API {
|
||||||
|
private axios: AxiosInstance;
|
||||||
|
|
||||||
|
public baseHeader: Header;
|
||||||
|
|
||||||
|
public baseToken: string;
|
||||||
|
|
||||||
|
public user: UserAPIProps;
|
||||||
|
|
||||||
|
constructor({ host }: APIParams) {
|
||||||
|
this.axios = createAxios({ host });
|
||||||
|
this.baseHeader = new Header();
|
||||||
|
this.baseToken = '';
|
||||||
|
this.user = CreatUserAPI({ axios: this.axios, header: this.baseHeader });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAccessToken = (token: string): void => {
|
||||||
|
this.baseHeader.updateToken(token);
|
||||||
|
this.baseToken = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
removeAccessToken = (): void => {
|
||||||
|
this.baseHeader.removeToken();
|
||||||
|
this.baseToken = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAccessTokenValid = async (): Promise<boolean> => {
|
||||||
|
const isJwtToken = await getIsJwtToken(this.baseToken);
|
||||||
|
return isJwtToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAccessTokenExpired = async (expiredTime = 0): Promise<boolean> => {
|
||||||
|
const isJwtToken = await this.checkAccessTokenValid();
|
||||||
|
if (isJwtToken) {
|
||||||
|
return getIsJwtTokenExpire(this.baseToken, expiredTime);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default API;
|
3
src/api/types.d.ts
vendored
Normal file
3
src/api/types.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface APIParams {
|
||||||
|
host: string;
|
||||||
|
}
|
79
src/api/user-apis/index.ts
Normal file
79
src/api/user-apis/index.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { AxiosInstance } from 'axios';
|
||||||
|
import Header from '../_APITool/Header';
|
||||||
|
import {
|
||||||
|
UserAPIProps,
|
||||||
|
PostUserRefreshAPIPromise,
|
||||||
|
GetUserSingleSignInAPIPromise,
|
||||||
|
GetUserAccountInfoAPIPromise,
|
||||||
|
PostUserSignOutAPIPromise,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
export function CreatUserAPI({ axios, header }: { axios: AxiosInstance; header: Header }): UserAPIProps {
|
||||||
|
return {
|
||||||
|
getUserSSO: async (backUrl: string): Promise<GetUserSingleSignInAPIPromise> => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/account/login/sso', {
|
||||||
|
params: {
|
||||||
|
back_url: backUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = {
|
||||||
|
url: '',
|
||||||
|
error: error.response?.data,
|
||||||
|
} as GetUserSingleSignInAPIPromise;
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
postUserRefreshToken: async (): Promise<PostUserRefreshAPIPromise> => {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/account/refresh_token', undefined, {
|
||||||
|
headers: { ...header.header },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = {
|
||||||
|
token: '',
|
||||||
|
error: error.response?.data,
|
||||||
|
};
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getUserAccountInfo: async (): Promise<GetUserAccountInfoAPIPromise> => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/account', {
|
||||||
|
headers: { ...header.header },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = {
|
||||||
|
account: {
|
||||||
|
_id: '',
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
display_name: '',
|
||||||
|
createdAt: '',
|
||||||
|
updatedAt: '',
|
||||||
|
error: error.response?.data,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
postUserSignOut: async (): Promise<PostUserSignOutAPIPromise> => {
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/account/logout', undefined, {
|
||||||
|
headers: { ...header.header },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = {
|
||||||
|
url: '',
|
||||||
|
error: error.response?.data,
|
||||||
|
};
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
27
src/api/user-apis/types.d.ts
vendored
Normal file
27
src/api/user-apis/types.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { APIError } from '@Models/GeneralTypes';
|
||||||
|
import {
|
||||||
|
UserTokenInfo,
|
||||||
|
UserSignInInfo,
|
||||||
|
UserOAuthUrl,
|
||||||
|
UserSignOutUrl,
|
||||||
|
} from '@Models/Redux/User/types';
|
||||||
|
|
||||||
|
export interface GetUserSingleSignInAPIPromise extends UserOAuthUrl {
|
||||||
|
error?: APIError;
|
||||||
|
}
|
||||||
|
export interface PostUserRefreshAPIPromise extends UserTokenInfo {
|
||||||
|
error?: APIError;
|
||||||
|
}
|
||||||
|
export interface GetUserAccountInfoAPIPromise extends UserSignInInfo {
|
||||||
|
error?: APIError;
|
||||||
|
}
|
||||||
|
export interface PostUserSignOutAPIPromise extends UserSignOutUrl {
|
||||||
|
error?: APIError;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAPIProps {
|
||||||
|
getUserSSO: (backUrl: string) => Promise<GetUserSingleSignInAPIPromise>;
|
||||||
|
postUserRefreshToken: () => Promise<PostUserRefreshAPIPromise>;
|
||||||
|
getUserAccountInfo: () => Promise<GetUserAccountInfoAPIPromise>;
|
||||||
|
postUserSignOut: () => Promise<PostUserSignOutAPIPromise>;
|
||||||
|
}
|
BIN
src/assets/loading.svg
Normal file
BIN
src/assets/loading.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/logo.png
Normal file
BIN
src/assets/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/route/active/home.svg
Normal file
BIN
src/assets/route/active/home.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 197 B |
BIN
src/assets/route/disactive/home.svg
Normal file
BIN
src/assets/route/disactive/home.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 197 B |
1
src/base/keycode/index.tsx
Normal file
1
src/base/keycode/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const ENTER_KEY_CODE = 13;
|
32
src/base/routes/Admin.ts
Normal file
32
src/base/routes/Admin.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import BASE_URL from '../url';
|
||||||
|
import { RouteObject } from './types';
|
||||||
|
|
||||||
|
const ADMIN_URL = BASE_URL.ADMIN;
|
||||||
|
|
||||||
|
const Admin: RouteObject = {
|
||||||
|
main: [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
route: ADMIN_URL.ROOT_PAGE_HOME,
|
||||||
|
lang: 'frontend.global.property.routehome',
|
||||||
|
component: import('@Components/Pages/Home'),
|
||||||
|
exact: false,
|
||||||
|
subRoute: [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
route: `${ADMIN_URL.PAGE_HOME_OVERVIEW}`,
|
||||||
|
lang: '',
|
||||||
|
component: import('@Components/Pages/Home'),
|
||||||
|
exact: false,
|
||||||
|
subRoute: [],
|
||||||
|
avatar: '',
|
||||||
|
avatarActive: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
avatar: 'route/disactive/home.svg',
|
||||||
|
avatarActive: 'route/active/home.svg',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Admin;
|
29
src/base/routes/Guest.ts
Normal file
29
src/base/routes/Guest.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import BASE_URL from '../url';
|
||||||
|
import { RouteObject } from './types';
|
||||||
|
|
||||||
|
const GUEST_URL = BASE_URL.GUEST;
|
||||||
|
|
||||||
|
const Guest: RouteObject = {
|
||||||
|
registration: [
|
||||||
|
{
|
||||||
|
name: 'SignIn',
|
||||||
|
route: `${GUEST_URL.BASE_PAGE_SIGN_IN}`,
|
||||||
|
component: import('@Components/Pages/SignIn'),
|
||||||
|
exact: false,
|
||||||
|
subRoute: [],
|
||||||
|
avatar: '',
|
||||||
|
avatarActive: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'OAuth',
|
||||||
|
route: `${GUEST_URL.BASE_PAGE_OAUTH}`,
|
||||||
|
component: import('@Components/Pages/OAuth'),
|
||||||
|
exact: false,
|
||||||
|
subRoute: [],
|
||||||
|
avatar: '',
|
||||||
|
avatarActive: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Guest;
|
7
src/base/routes/index.ts
Normal file
7
src/base/routes/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Guest from './Guest';
|
||||||
|
import Admin from './Admin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
Guest,
|
||||||
|
Admin,
|
||||||
|
};
|
23
src/base/routes/types.d.ts
vendored
Normal file
23
src/base/routes/types.d.ts
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { LazyExoticComponent } from 'react';
|
||||||
|
import { EnvConfig } from '@Env/types';
|
||||||
|
import { ModuleKey } from '@Models/Redux/User/types';
|
||||||
|
|
||||||
|
export { ModuleKey };
|
||||||
|
|
||||||
|
export interface RouteItem {
|
||||||
|
name: string;
|
||||||
|
route: string;
|
||||||
|
lang?: string;
|
||||||
|
component: LazyExoticComponent;
|
||||||
|
subRoute: RouteItem[];
|
||||||
|
exact: boolean;
|
||||||
|
avatar: string;
|
||||||
|
avatarActive: string;
|
||||||
|
permissions?: ModuleKey[];
|
||||||
|
openNewWindow?: boolean;
|
||||||
|
openNewWindowKey?: (keyof EnvConfig)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RouteObject {
|
||||||
|
[key: string]: RouteItem[];
|
||||||
|
}
|
1
src/base/timezone/index.ts
Normal file
1
src/base/timezone/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const TIMEZONE_TAIPEI = 'Asia/Taipei';
|
3
src/base/url/Admin.ts
Normal file
3
src/base/url/Admin.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/* 首頁 */
|
||||||
|
export const ROOT_PAGE_HOME = '/home';
|
||||||
|
export const PAGE_HOME_OVERVIEW = '/overview';
|
5
src/base/url/Guest.ts
Normal file
5
src/base/url/Guest.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/* 使用者登入 */
|
||||||
|
export const BASE_PAGE_SIGN_IN = '/login';
|
||||||
|
|
||||||
|
/* 第三方登入 */
|
||||||
|
export const BASE_PAGE_OAUTH = '/oauth_check';
|
7
src/base/url/index.ts
Normal file
7
src/base/url/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import * as GUEST from './Guest';
|
||||||
|
import * as ADMIN from './Admin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
GUEST,
|
||||||
|
ADMIN,
|
||||||
|
};
|
6
src/components/App/index.module.css
Normal file
6
src/components/App/index.module.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.appContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
29
src/components/App/index.tsx
Normal file
29
src/components/App/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React, { memo, useMemo } from 'react';
|
||||||
|
import useMappedState from '@Hooks/useMappedState';
|
||||||
|
import MainAppView from '@Components/Layer/MainAppView';
|
||||||
|
import MainAppCheckView from '@Components/Layer/MainAppCheckView';
|
||||||
|
import ModalDialog from '@Components/Common/Modals/ModalDialog';
|
||||||
|
import ModalConfirm from '@Components/Common/Modals/ModalConfirm';
|
||||||
|
import { AppProps } from './types';
|
||||||
|
|
||||||
|
const App: React.FC<AppProps> = ({ Router, routerProps }): React.ReactElement => {
|
||||||
|
/* Global & Local State */
|
||||||
|
const storeUser = useMappedState((state) => state.user);
|
||||||
|
/* Views */
|
||||||
|
const RenderMainView = useMemo(() => {
|
||||||
|
if (storeUser.userAuthCheck) {
|
||||||
|
return <MainAppView />;
|
||||||
|
}
|
||||||
|
return <MainAppCheckView />;
|
||||||
|
}, [storeUser.userAuthCheck]);
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<Router {...routerProps}>
|
||||||
|
{RenderMainView}
|
||||||
|
<ModalDialog />
|
||||||
|
<ModalConfirm />
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(App);
|
8
src/components/App/types.d.ts
vendored
Normal file
8
src/components/App/types.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { BrowserRouterProps } from 'react-router-dom';
|
||||||
|
import { StaticRouterProps } from 'react-router';
|
||||||
|
|
||||||
|
export interface AppProps {
|
||||||
|
Router: React.ComponentClass<Record<string, unknown>>;
|
||||||
|
routerProps?: BrowserRouterProps | StaticRouterProps;
|
||||||
|
}
|
12
src/components/Base/GifLoader/index.module.css
Normal file
12
src/components/Base/GifLoader/index.module.css
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.gifLoaderContainer) {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.gifLoaderStyle) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
15
src/components/Base/GifLoader/index.tsx
Normal file
15
src/components/Base/GifLoader/index.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { loadImage } from '@Tools/image-loader';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
function GifLoader(): React.ReactElement {
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<div className={classNames(Styles.gifLoaderContainer)}>
|
||||||
|
<img className={classNames(Styles.gifLoaderStyle)} alt="dipp-gif-loading" src={loadImage('loading.svg')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(GifLoader);
|
47
src/components/Base/Lazy/index.tsx
Normal file
47
src/components/Base/Lazy/index.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import React, {
|
||||||
|
lazy, Suspense, useMemo, useState, useEffect,
|
||||||
|
} from 'react';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import Skeleton from '@material-ui/lab/Skeleton';
|
||||||
|
import Loading from '@Components/Base/Loading';
|
||||||
|
import { Props, ComponentState } from './types';
|
||||||
|
|
||||||
|
const Components: { [key: string]: ComponentState } = {};
|
||||||
|
|
||||||
|
const Lazy: React.FC<Props> = (props): React.ReactElement => {
|
||||||
|
const { componentImport, componentChunkName, componentProps } = props;
|
||||||
|
const [isRender, setIsRender] = useState(false);
|
||||||
|
const RenderComponent = useMemo(() => {
|
||||||
|
const Component = Components[componentChunkName];
|
||||||
|
if (Component) {
|
||||||
|
return <Component {...componentProps} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
|
<Loading typePosition="relative" typeZIndex={10005} typeIcon="line:fix" isLoading />
|
||||||
|
<Skeleton variant="rect" width="100%" height="100%" animation="wave" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}, [componentChunkName, isRender, componentProps]);
|
||||||
|
/* Hooks */
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
if (!Components[componentChunkName]) {
|
||||||
|
Components[componentChunkName] = lazy(async () => {
|
||||||
|
const Element = await componentImport;
|
||||||
|
return Element;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsRender(cloneDeep(!isRender));
|
||||||
|
})();
|
||||||
|
}, [componentChunkName]);
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Suspense fallback={<Loading typePosition="relative" typeZIndex={10005} typeIcon="line:fix" isLoading />}>
|
||||||
|
{RenderComponent}
|
||||||
|
</Suspense>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Lazy;
|
13
src/components/Base/Lazy/types.d.ts
vendored
Normal file
13
src/components/Base/Lazy/types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { ComponentClass, LazyExoticComponent } from 'react';
|
||||||
|
|
||||||
|
export type ComponentState = LazyExoticComponent | null;
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
componentImport: Promise<{ default: LazyExoticComponent<ComponentClass> }>;
|
||||||
|
componentChunkName: string;
|
||||||
|
componentProps: { [key: string] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
Component: ComponentClass;
|
||||||
|
}
|
110
src/components/Base/Loading/index.module.css
Normal file
110
src/components/Base/Loading/index.module.css
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
/* General Style */
|
||||||
|
:local(.flexCentral) {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes iconLoadAnim {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes textLoadAnim {
|
||||||
|
to {
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Component Style */
|
||||||
|
:local(.fullScreenLoadingContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
:local(.relateScreenLoadingContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
:local(.backgroundBlack) {
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
:local(.backgroundWhite) {
|
||||||
|
background: rgb(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
:local(.backgroundBlackImage) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
:local(.backgroundWhiteImage) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
background-size: cover;
|
||||||
|
}
|
||||||
|
:local(.loadingTextWhite) {
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #e02020;
|
||||||
|
}
|
||||||
|
:local(.loadingTextBlack) {
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
:local(.loadingAreaContainer){
|
||||||
|
height: 70px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
:local(.loadingIconStyle) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
animation: iconLoadAnim 1.2s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
:local(.loadingTextStyle) {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
:local(.loadingTextStyle::before){
|
||||||
|
font-style: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
:local(.loadingTextStyle::after) {
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: bottom;
|
||||||
|
animation: textLoadAnim steps(4, end) 1.2s infinite;
|
||||||
|
content: "\2026";
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
:local(.loadingZIndex) {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
109
src/components/Base/Loading/index.tsx
Normal file
109
src/components/Base/Loading/index.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { memo, useMemo } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||||
|
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||||
|
import { LoadingProps } from './types';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
function Loading(props: LoadingProps): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const {
|
||||||
|
typePosition, typeBackground, typeZIndex, typeIcon, isLoading, isHideText, text,
|
||||||
|
} = props;
|
||||||
|
/* Views */
|
||||||
|
const RenderPosition = useMemo(() => {
|
||||||
|
switch (typePosition) {
|
||||||
|
case 'relative':
|
||||||
|
return Styles.relateScreenLoadingContainer;
|
||||||
|
case 'absolute':
|
||||||
|
return Styles.fullScreenLoadingContainer;
|
||||||
|
default:
|
||||||
|
return Styles.fullScreenLoadingContainer;
|
||||||
|
}
|
||||||
|
}, [typePosition]);
|
||||||
|
const RenderBackground = useMemo(() => {
|
||||||
|
switch (typeBackground) {
|
||||||
|
case 'white':
|
||||||
|
return Styles.backgroundWhite;
|
||||||
|
case 'black':
|
||||||
|
return Styles.backgroundBlack;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [typeBackground]);
|
||||||
|
const RenderTextColor = useMemo(() => {
|
||||||
|
switch (typeBackground) {
|
||||||
|
case 'white':
|
||||||
|
return Styles.loadingTextWhite;
|
||||||
|
case 'black':
|
||||||
|
return Styles.loadingTextBlack;
|
||||||
|
default:
|
||||||
|
return Styles.loadingTextWhite;
|
||||||
|
}
|
||||||
|
}, [typeBackground]);
|
||||||
|
const RenderText = useMemo(() => {
|
||||||
|
if (isHideText) {
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
return <div className={classNames(RenderTextColor, Styles.loadingTextStyle)}>{text}</div>;
|
||||||
|
}, [isHideText, text, typeBackground]);
|
||||||
|
const RenderAnimation = useMemo(() => {
|
||||||
|
switch (typeIcon) {
|
||||||
|
case 'basic':
|
||||||
|
return (
|
||||||
|
<div className={`${Styles.loadingAreaContainer}`}>
|
||||||
|
<CircularProgress size={25} thickness={5} />
|
||||||
|
{RenderText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'text':
|
||||||
|
return <div className={`${Styles.loadingAreaContainer}`}>{RenderText}</div>;
|
||||||
|
case 'icon':
|
||||||
|
return (
|
||||||
|
<div className={`${Styles.loadingAreaContainer}`}>
|
||||||
|
<CircularProgress size={25} thickness={5} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'line:fix':
|
||||||
|
return <LinearProgress />;
|
||||||
|
case 'line:relative':
|
||||||
|
return <LinearProgress />;
|
||||||
|
default:
|
||||||
|
return <React.Fragment />;
|
||||||
|
}
|
||||||
|
}, [typeIcon, isHideText, text]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isLoading && (
|
||||||
|
<div
|
||||||
|
className={classNames(RenderPosition, RenderBackground, Styles.flexCentral)}
|
||||||
|
style={{ zIndex: typeZIndex }}
|
||||||
|
>
|
||||||
|
{RenderAnimation}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading.propTypes = {
|
||||||
|
typePosition: PropTypes.string,
|
||||||
|
typeBackground: PropTypes.string,
|
||||||
|
typeZIndex: PropTypes.number,
|
||||||
|
typeIcon: PropTypes.string,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
isHideText: PropTypes.bool,
|
||||||
|
text: PropTypes.string,
|
||||||
|
};
|
||||||
|
Loading.defaultProps = {
|
||||||
|
typePosition: 'relative',
|
||||||
|
typeBackground: '',
|
||||||
|
typeZIndex: 10000,
|
||||||
|
typeIcon: 'line:relative',
|
||||||
|
isLoading: false,
|
||||||
|
isHideText: false,
|
||||||
|
text: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Loading);
|
9
src/components/Base/Loading/types.d.ts
vendored
Normal file
9
src/components/Base/Loading/types.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface LoadingProps {
|
||||||
|
typePosition: 'relative' | 'absolute';
|
||||||
|
typeBackground: 'white' | 'black';
|
||||||
|
typeZIndex: number;
|
||||||
|
typeIcon: 'basic' | 'text' | 'icon' | 'line:fix' | 'line:relative';
|
||||||
|
isLoading: boolean;
|
||||||
|
isHideText?: boolean;
|
||||||
|
text?: string;
|
||||||
|
}
|
32
src/components/Base/Modal/index.module.css
Normal file
32
src/components/Base/Modal/index.module.css
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.modalContainer) {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.modalContainerTitle) {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.modalContainerContent) {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.modalContainerAction) {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 18px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.modalContainerActionStyle) {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
:local(.modalContainerRemoveIcon) {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
top: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
:local(.modalContainerRemoveIconStyle) {
|
||||||
|
color: #000000 !important;
|
||||||
|
}
|
138
src/components/Base/Modal/index.tsx
Normal file
138
src/components/Base/Modal/index.tsx
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import React, { memo, useMemo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import ClearIcon from '@material-ui/icons/Clear';
|
||||||
|
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||||
|
import Dialog from '@material-ui/core/Dialog';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Loading from '@Components/Base/Loading';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
import { ModalProps } from './types';
|
||||||
|
|
||||||
|
function Modal(props: ModalProps): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const {
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
typeSize,
|
||||||
|
typeIsLoading,
|
||||||
|
disableEscapeKeyDown,
|
||||||
|
disableBackdropClick,
|
||||||
|
disableCancelButton,
|
||||||
|
disableConfirmButton,
|
||||||
|
confirmButtonText,
|
||||||
|
cancelButtonText,
|
||||||
|
tipsText,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
mainName,
|
||||||
|
titleClassName,
|
||||||
|
actionClassName,
|
||||||
|
title,
|
||||||
|
closeIcon = true,
|
||||||
|
disabledConfirm,
|
||||||
|
} = props;
|
||||||
|
/* Views */
|
||||||
|
const RenderSize = useMemo(() => {
|
||||||
|
switch (typeSize) {
|
||||||
|
case 'xs':
|
||||||
|
return 'xs';
|
||||||
|
case 'sm':
|
||||||
|
return 'sm';
|
||||||
|
case 'md':
|
||||||
|
return 'md';
|
||||||
|
case 'lg':
|
||||||
|
return 'lg';
|
||||||
|
case 'xl':
|
||||||
|
return 'xl';
|
||||||
|
default:
|
||||||
|
return 'sm';
|
||||||
|
}
|
||||||
|
}, [typeSize]);
|
||||||
|
const RenderCloseIcon = useMemo<React.ReactElement>(() => {
|
||||||
|
if (closeIcon) {
|
||||||
|
return (
|
||||||
|
<ButtonBase className={classNames(Styles.modalContainerRemoveIcon)} onClick={onClose}>
|
||||||
|
<ClearIcon className={classNames(Styles.modalContainerRemoveIconStyle)} fontSize="small" />
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}, [open, closeIcon, onClose]);
|
||||||
|
const RenderTitle = useMemo<React.ReactElement>(() => {
|
||||||
|
if (title) {
|
||||||
|
return <div className={classNames(Styles.modalContainerTitle, titleClassName)}>{title}</div>;
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}, [title]);
|
||||||
|
const RenderIsLoading = useMemo<React.ReactElement>(
|
||||||
|
() => <Loading typePosition="relative" typeZIndex={10003} typeIcon="line:relative" isLoading={typeIsLoading} />,
|
||||||
|
[typeIsLoading],
|
||||||
|
);
|
||||||
|
const RenderCancelButton = useMemo<React.ReactElement>(() => {
|
||||||
|
if (!disableCancelButton) {
|
||||||
|
return (
|
||||||
|
<Button className={classNames(Styles.modalContainerActionStyle)} color="default" onClick={onClose}>
|
||||||
|
{!cancelButtonText ? '取消' : cancelButtonText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}, [disableCancelButton, cancelButtonText, onClose]);
|
||||||
|
const RenderConfirmButton = useMemo<React.ReactElement>(() => {
|
||||||
|
if (!disableConfirmButton) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={classNames(Styles.modalContainerActionStyle)}
|
||||||
|
color="primary"
|
||||||
|
variant="contained"
|
||||||
|
onClick={onConfirm}
|
||||||
|
disabled={disabledConfirm}
|
||||||
|
>
|
||||||
|
{!confirmButtonText ? '確定' : confirmButtonText}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}, [disableConfirmButton, confirmButtonText, onConfirm]);
|
||||||
|
const RenderTipsText = useMemo(() => {
|
||||||
|
if (tipsText) {
|
||||||
|
return tipsText;
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
}, [tipsText]);
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
className={classNames(Styles.modalContainer, mainName)}
|
||||||
|
open={open}
|
||||||
|
onClose={onClose}
|
||||||
|
fullWidth
|
||||||
|
maxWidth={RenderSize}
|
||||||
|
disableEnforceFocus
|
||||||
|
disableEscapeKeyDown={disableEscapeKeyDown}
|
||||||
|
disableBackdropClick={disableBackdropClick}
|
||||||
|
>
|
||||||
|
<div className={classNames(className)}>
|
||||||
|
{RenderCloseIcon}
|
||||||
|
{RenderTitle}
|
||||||
|
<div className={classNames(Styles.modalContainerContent)} style={{ marginTop: title ? '18px' : '0px' }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
(!disableCancelButton || !disableConfirmButton) && Styles.modalContainerAction,
|
||||||
|
actionClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{RenderTipsText}
|
||||||
|
{RenderCancelButton}
|
||||||
|
{RenderConfirmButton}
|
||||||
|
</div>
|
||||||
|
{RenderIsLoading}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Modal);
|
29
src/components/Base/Modal/types.d.ts
vendored
Normal file
29
src/components/Base/Modal/types.d.ts
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MessageSnackObject } from '@Models/Redux/Message/types';
|
||||||
|
|
||||||
|
export type ModalSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||||
|
|
||||||
|
export interface ModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
typeSize?: ModalSize;
|
||||||
|
typeIsLoading?: boolean;
|
||||||
|
disableEscapeKeyDown?: boolean;
|
||||||
|
disableBackdropClick?: boolean;
|
||||||
|
disableCancelButton?: boolean;
|
||||||
|
disableConfirmButton?: boolean;
|
||||||
|
confirmButtonText?: string;
|
||||||
|
cancelButtonText?: string;
|
||||||
|
tipsText?: React.ReactNode;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
mainName?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
actionClassName?: string;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
closeIcon?: boolean;
|
||||||
|
disabledConfirm?: boolean;
|
||||||
|
blockLeave?: boolean;
|
||||||
|
blockMessage?: MessageSnackObject;
|
||||||
|
}
|
34
src/components/Common/Modals/ModalConfirm/index.module.css
Normal file
34
src/components/Common/Modals/ModalConfirm/index.module.css
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.confirmItemContainer) {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.confirmItemTitle) {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:local(.confirmItemSubTitle) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 23px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.confirmItemTitleStyle) {
|
||||||
|
line-height: 36px !important;
|
||||||
|
}
|
||||||
|
:local(.confirmItemActionContainer) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 23px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.confirmItemActionButtonStyle) {
|
||||||
|
margin-left: 15px !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
98
src/components/Common/Modals/ModalConfirm/index.tsx
Normal file
98
src/components/Common/Modals/ModalConfirm/index.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { memo, useMemo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import useLang from '@Hooks/useLang';
|
||||||
|
import useMessage from '@Hooks/useMessage';
|
||||||
|
import useMappedState from '@Hooks/useMappedState';
|
||||||
|
import Modal from '@Components/Base/Modal';
|
||||||
|
import { ConfirmItemObject } from './types';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
/* Confirm Item */
|
||||||
|
function ConfirmItem(props: ConfirmItemObject): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const { i18n } = useLang();
|
||||||
|
const { confirm, requestRemoveConfirm } = props;
|
||||||
|
/* Functions */
|
||||||
|
const onConfirmClick = (): void => {
|
||||||
|
if (confirm.onConfirm) confirm.onConfirm();
|
||||||
|
requestRemoveConfirm();
|
||||||
|
};
|
||||||
|
const onCancelClick = (): void => {
|
||||||
|
if (confirm.onCancel) confirm.onCancel();
|
||||||
|
requestRemoveConfirm();
|
||||||
|
};
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<div className={classNames(Styles.confirmItemContainer)}>
|
||||||
|
<div className={classNames(Styles.confirmItemTitle)}>
|
||||||
|
<Typography className={classNames(Styles.confirmItemTitleStyle)} variant="h2" color="textPrimary">
|
||||||
|
{confirm.typeTitle}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(Styles.confirmItemSubTitle)}>
|
||||||
|
<Typography variant="body1" color="textPrimary" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{confirm.typeContent && confirm.typeContent}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(Styles.confirmItemActionContainer)}>
|
||||||
|
<Button
|
||||||
|
className={classNames(Styles.confirmItemActionButtonStyle)}
|
||||||
|
variant="text"
|
||||||
|
color="default"
|
||||||
|
onClick={onCancelClick}
|
||||||
|
>
|
||||||
|
{confirm.cancelText ? confirm.cancelText : i18n.t('frontend.global.operation.cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={classNames(Styles.confirmItemActionButtonStyle)}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={onConfirmClick}
|
||||||
|
>
|
||||||
|
{confirm.confirmText ? confirm.confirmText : i18n.t('frontend.global.operation.confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalConfirm(): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const reduxMessage = useMessage();
|
||||||
|
const storeMessage = useMappedState((state) => state.message);
|
||||||
|
/* Functions */
|
||||||
|
const requestRemoveConfirm = (): void => {
|
||||||
|
reduxMessage.removeConfirm();
|
||||||
|
};
|
||||||
|
/* Views */
|
||||||
|
const RenderMessages = useMemo(() => {
|
||||||
|
if (storeMessage.messageConfirmList.list.length > 0) {
|
||||||
|
return (
|
||||||
|
<ConfirmItem confirm={storeMessage.messageConfirmList.list[0]} requestRemoveConfirm={requestRemoveConfirm} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <React.Fragment />;
|
||||||
|
}, [storeMessage.messageConfirmList]);
|
||||||
|
const RenderIsMessageOpen = useMemo(() => {
|
||||||
|
if (storeMessage.messageConfirmList.list.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [storeMessage.messageConfirmList]);
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={RenderIsMessageOpen}
|
||||||
|
onClose={requestRemoveConfirm}
|
||||||
|
typeSize="sm"
|
||||||
|
disableCancelButton
|
||||||
|
disableConfirmButton
|
||||||
|
>
|
||||||
|
{RenderMessages}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ModalConfirm);
|
6
src/components/Common/Modals/ModalConfirm/types.d.ts
vendored
Normal file
6
src/components/Common/Modals/ModalConfirm/types.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { MessageConfirmObject } from '@Models/Redux/Message/types';
|
||||||
|
|
||||||
|
export interface ConfirmItemObject {
|
||||||
|
confirm: MessageConfirmObject;
|
||||||
|
requestRemoveConfirm: () => void;
|
||||||
|
}
|
46
src/components/Common/Modals/ModalDialog/index.module.css
Normal file
46
src/components/Common/Modals/ModalDialog/index.module.css
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.dialogItemContainer) {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.dialogItemTitle) {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
:local(.dialogItemSubTitle) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 23px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.dialogItemDebug) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 23px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
|
padding: 16px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
:local(.dialogItemTitleStyle) {
|
||||||
|
line-height: 36px !important;
|
||||||
|
}
|
||||||
|
:local(.dialogItemActionContainer) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 23px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.dialogItemDebugAction) {
|
||||||
|
left: -7px;
|
||||||
|
bottom: -2px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
:local(.dialogItemActionButtonStyle) {
|
||||||
|
position: relative;
|
||||||
|
}
|
103
src/components/Common/Modals/ModalDialog/index.tsx
Normal file
103
src/components/Common/Modals/ModalDialog/index.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
import React, { memo, useMemo, useState } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import useLang from '@Hooks/useLang';
|
||||||
|
import useMessage from '@Hooks/useMessage';
|
||||||
|
import useMappedState from '@Hooks/useMappedState';
|
||||||
|
import Modal from '@Components/Base/Modal';
|
||||||
|
import { DialogItemObject } from './types';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
/* Dialog Item */
|
||||||
|
function DialogItem(props: DialogItemObject): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const { i18n } = useLang();
|
||||||
|
const { dialog, requestRemoveDialog } = props;
|
||||||
|
const [hiddenDebugMessage, setHiddenDebugMessage] = useState(false);
|
||||||
|
/* Functions */
|
||||||
|
const onConfirmClick = (): void => {
|
||||||
|
if (dialog.onConfirm) dialog.onConfirm();
|
||||||
|
requestRemoveDialog();
|
||||||
|
};
|
||||||
|
/* Views */
|
||||||
|
return (
|
||||||
|
<div className={classNames(Styles.dialogItemContainer)}>
|
||||||
|
<div className={classNames(Styles.dialogItemTitle)}>
|
||||||
|
<Typography className={classNames(Styles.dialogItemTitleStyle)} variant="h2" color="textPrimary">
|
||||||
|
{dialog.typeTitle}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(Styles.dialogItemSubTitle)}>
|
||||||
|
<Typography variant="body1" color="textPrimary" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{dialog.typeContent && dialog.typeContent}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
{dialog.typeHiddenMessage && hiddenDebugMessage && (
|
||||||
|
<div className={classNames(Styles.dialogItemDebug)}>
|
||||||
|
<Typography variant="body2" color="textSecondary" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{dialog.typeHiddenMessage && dialog.typeHiddenMessage}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={classNames(Styles.dialogItemActionContainer)}>
|
||||||
|
{dialog.typeHiddenMessage && (
|
||||||
|
<Button
|
||||||
|
className={classNames(Styles.dialogItemDebugAction)}
|
||||||
|
variant="text"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setHiddenDebugMessage(!hiddenDebugMessage)}
|
||||||
|
>
|
||||||
|
{hiddenDebugMessage ? i18n.t('frontend.global.operation.debugopenclose') : i18n.t('frontend.global.operation.debugopen')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
className={classNames(Styles.dialogItemActionButtonStyle)}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={onConfirmClick}
|
||||||
|
>
|
||||||
|
{dialog.confirmText ? dialog.confirmText : i18n.t('frontend.global.operation.confirm')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ModalDialog(): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const reduxMessage = useMessage();
|
||||||
|
const storeMessage = useMappedState(state => state.message);
|
||||||
|
/* Functions */
|
||||||
|
const requestRemoveDialog = (): void => {
|
||||||
|
reduxMessage.removeDialog();
|
||||||
|
};
|
||||||
|
/* Views */
|
||||||
|
const RenderMessages = useMemo(() => {
|
||||||
|
if (storeMessage.messageDialogList.list.length > 0) {
|
||||||
|
return <DialogItem dialog={storeMessage.messageDialogList.list[0]} requestRemoveDialog={requestRemoveDialog} />;
|
||||||
|
}
|
||||||
|
return <React.Fragment />;
|
||||||
|
}, [storeMessage.messageDialogList]);
|
||||||
|
const RenderIsMessageOpen = useMemo(() => {
|
||||||
|
if (storeMessage.messageDialogList.list.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}, [storeMessage.messageDialogList]);
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={RenderIsMessageOpen}
|
||||||
|
onClose={requestRemoveDialog}
|
||||||
|
typeSize="sm"
|
||||||
|
disableCancelButton
|
||||||
|
disableConfirmButton
|
||||||
|
>
|
||||||
|
{RenderMessages}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ModalDialog);
|
6
src/components/Common/Modals/ModalDialog/types.d.ts
vendored
Normal file
6
src/components/Common/Modals/ModalDialog/types.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { MessageDialogObject } from '@Models/Redux/Message/types';
|
||||||
|
|
||||||
|
export interface DialogItemObject {
|
||||||
|
dialog: MessageDialogObject;
|
||||||
|
requestRemoveDialog: () => void;
|
||||||
|
}
|
11
src/components/Common/Routes/index.tsx
Normal file
11
src/components/Common/Routes/index.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import useMappedRoute from '@Hooks/useMappedRoute';
|
||||||
|
|
||||||
|
function Routes(): React.ReactElement {
|
||||||
|
/* Global & Local State */
|
||||||
|
const routeDom = useMappedRoute([], 'home', 'login');
|
||||||
|
/* Main */
|
||||||
|
return <>{routeDom}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(Routes);
|
10
src/components/Layer/MainAppCheckView/index.module.css
Normal file
10
src/components/Layer/MainAppCheckView/index.module.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
:local(.appCheckViewContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
23
src/components/Layer/MainAppCheckView/index.tsx
Normal file
23
src/components/Layer/MainAppCheckView/index.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import useDidMount from '@Hooks/useDidMount';
|
||||||
|
import useReduxApi from '@Hooks/useReduxApi';
|
||||||
|
import GifLoader from '@Components/Base/GifLoader';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
function AppCheckView(): React.ReactElement {
|
||||||
|
/* Global & Local States */
|
||||||
|
const reduxUser = useReduxApi('user');
|
||||||
|
/* Functions */
|
||||||
|
const initialize = (): void => {
|
||||||
|
reduxUser('getUserIsLogin', []);
|
||||||
|
};
|
||||||
|
/* Hooks */
|
||||||
|
useDidMount(() => {
|
||||||
|
initialize();
|
||||||
|
});
|
||||||
|
/* Main */
|
||||||
|
return <div className={classNames(Styles.appCheckViewContainer)}><GifLoader /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AppCheckView);
|
39
src/components/Layer/MainAppView/index.module.css
Normal file
39
src/components/Layer/MainAppView/index.module.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.appContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
background: #f4f4f4;
|
||||||
|
}
|
||||||
|
:local(.appHeader) {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 60px !important;
|
||||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2);
|
||||||
|
background: #ffffff;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1300;
|
||||||
|
}
|
||||||
|
:local(.appBody) {
|
||||||
|
width: 100% !important;
|
||||||
|
height: calc(100% - 60px) !important;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
:local(.appHeart) {
|
||||||
|
width: calc(100% - 230px);
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.appSide) {
|
||||||
|
max-width: 230px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.appMain) {
|
||||||
|
flex: 1 1;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
17
src/components/Layer/MainAppView/index.tsx
Normal file
17
src/components/Layer/MainAppView/index.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Routes from '@Components/Common/Routes';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
function MainAppView(): React.ReactElement {
|
||||||
|
/* Global & Local State */
|
||||||
|
/* Data */
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<div className={classNames(Styles.appContainer)}>
|
||||||
|
<Routes />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(MainAppView);
|
6
src/components/Pages/Home/index.module.css
Normal file
6
src/components/Pages/Home/index.module.css
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.homeContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
10
src/components/Pages/Home/index.tsx
Normal file
10
src/components/Pages/Home/index.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
function Home(): React.ReactElement {
|
||||||
|
/* Global & Local State */
|
||||||
|
/* Main */
|
||||||
|
return <div className={classNames(Styles.homeContainer)}>Home</div>;
|
||||||
|
}
|
||||||
|
export default memo(Home);
|
10
src/components/Pages/OAuth/index.module.css
Normal file
10
src/components/Pages/OAuth/index.module.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.oAuthContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: fixed;
|
||||||
|
top: 0px;
|
||||||
|
left: 0px;
|
||||||
|
background: white;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
80
src/components/Pages/OAuth/index.tsx
Normal file
80
src/components/Pages/OAuth/index.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { memo } from 'react';
|
||||||
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
import qs from 'qs';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import useLang from '@Hooks/useLang';
|
||||||
|
import useMessage from '@Hooks/useMessage';
|
||||||
|
import usaDidMount from '@Hooks/useDidMount';
|
||||||
|
import useReduxApi from '@Hooks/useReduxApi';
|
||||||
|
import Loading from '@Components/Base/Loading';
|
||||||
|
import { UserOAuthResponse, OAuthResult, APIError } from './types';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
function OAuth(): React.ReactElement {
|
||||||
|
/* Global & Local State */
|
||||||
|
const { i18n } = useLang();
|
||||||
|
const reduxMessage = useMessage();
|
||||||
|
const reduxUser = useReduxApi('user');
|
||||||
|
const routeHistory = useHistory();
|
||||||
|
const routeLocation = useLocation();
|
||||||
|
/* Functions */
|
||||||
|
const onOAuthSuccess = (token: string): void => {
|
||||||
|
reduxUser('postUserTokenInfoSignIn', [token]);
|
||||||
|
};
|
||||||
|
const onOAuthFailed = (error: APIError): void => {
|
||||||
|
reduxMessage.error(error);
|
||||||
|
routeHistory.push('/');
|
||||||
|
};
|
||||||
|
const parseQuery = (base64String: string): OAuthResult => {
|
||||||
|
const result = Base64.decode(base64String);
|
||||||
|
const json: OAuthResult = JSON.parse(result);
|
||||||
|
return json;
|
||||||
|
};
|
||||||
|
const validateByType = (query: UserOAuthResponse): void => {
|
||||||
|
if (Object.keys(query).length > 0) {
|
||||||
|
if (query.error) {
|
||||||
|
const parseResult = parseQuery(query.error);
|
||||||
|
const error: APIError = {
|
||||||
|
code: parseResult.code,
|
||||||
|
message: parseResult.message,
|
||||||
|
errorStack: parseResult.errorStack,
|
||||||
|
errorMessage: parseResult.errorMessage,
|
||||||
|
};
|
||||||
|
onOAuthFailed(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (query.success) {
|
||||||
|
const parseResult = parseQuery(query.success);
|
||||||
|
onOAuthSuccess(parseResult.token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const initialize = async (): Promise<void> => {
|
||||||
|
const resultQuery: UserOAuthResponse = qs.parse(routeLocation.search, {
|
||||||
|
ignoreQueryPrefix: true,
|
||||||
|
});
|
||||||
|
if (Object.keys(resultQuery).length > 0) {
|
||||||
|
validateByType(resultQuery);
|
||||||
|
} else {
|
||||||
|
routeHistory.push('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/* Hooks */
|
||||||
|
usaDidMount(() => {
|
||||||
|
initialize();
|
||||||
|
});
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<div className={classNames(Styles.oAuthContainer)}>
|
||||||
|
<Loading
|
||||||
|
typeIcon="basic"
|
||||||
|
typePosition="absolute"
|
||||||
|
typeBackground="white"
|
||||||
|
isLoading
|
||||||
|
text={i18n.t('frontend.global.property.validate')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default memo(OAuth);
|
11
src/components/Pages/OAuth/types.d.ts
vendored
Normal file
11
src/components/Pages/OAuth/types.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { APIError } from '@Models/GeneralTypes';
|
||||||
|
import { UserOAuthResponse, UserTokenInfo } from '@Models/Redux/User/types';
|
||||||
|
|
||||||
|
export interface OAuthResult extends UserTokenInfo, APIError {
|
||||||
|
code: number;
|
||||||
|
message: string;
|
||||||
|
errorStack: string;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { UserOAuthResponse, UserTokenInfo, APIError };
|
77
src/components/Pages/SignIn/index.module.css
Normal file
77
src/components/Pages/SignIn/index.module.css
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
/* Component Style */
|
||||||
|
:local(.signInContainer) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInPanelContainer) {
|
||||||
|
padding: 0px calc(100% / 12) !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInTitleContainer) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 40px 0px 0px 0px;
|
||||||
|
position: relative;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
:local(.signInFormContainer) {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInActionContainer) {
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInTipsContainer) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 50px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInLanguageContainer) {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 50px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInFormMargin) {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
:local(.signInActionButtonStyle) {
|
||||||
|
min-width: 182px !important;
|
||||||
|
margin-right: 23px !important;
|
||||||
|
}
|
||||||
|
:local(.signInActionOrStyle) {
|
||||||
|
margin-bottom: 24px !important;
|
||||||
|
}
|
||||||
|
:local(.signInLanguageStyle) {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
}
|
||||||
|
:local(.signInTitleMainStyle) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
:local(.signInTitleSubStyle) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Style */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
:local(.signInActionButtonStyle) {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 0px !important;
|
||||||
|
margin-bottom: 23px !important;
|
||||||
|
}
|
||||||
|
:local(.signInActionButtonGoogleStyle) {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 0px !important;
|
||||||
|
margin-bottom: 23px !important;
|
||||||
|
}
|
||||||
|
}
|
68
src/components/Pages/SignIn/index.tsx
Normal file
68
src/components/Pages/SignIn/index.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { memo, useState, useEffect } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import useLang from '@Hooks/useLang';
|
||||||
|
import useDidMount from '@Hooks/useDidMount';
|
||||||
|
import useReduxApi from '@Hooks/useReduxApi';
|
||||||
|
import useMappedState from '@Hooks/useMappedState';
|
||||||
|
import Button from '@material-ui/core/Button';
|
||||||
|
import Typography from '@material-ui/core/Typography';
|
||||||
|
import Loading from '@Components/Base/Loading';
|
||||||
|
import BASE_URL from '@Base/url';
|
||||||
|
import Styles from './index.module.css';
|
||||||
|
|
||||||
|
const GUEST_URL = BASE_URL.GUEST;
|
||||||
|
|
||||||
|
function SignIn(): React.ReactElement {
|
||||||
|
/* Global & Local State */
|
||||||
|
const { i18n } = useLang();
|
||||||
|
const reduxUser = useReduxApi('user');
|
||||||
|
const storeUser = useMappedState((state) => state.user);
|
||||||
|
const storeGlobal = useMappedState((state) => state.global);
|
||||||
|
const [isFirstInitial, setIsFirstInitial] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
/* Functions */
|
||||||
|
const onSubmitSSO = (): void => {
|
||||||
|
const backUrl = `${window.location.origin}${GUEST_URL.BASE_PAGE_OAUTH}`;
|
||||||
|
setIsLoading(true);
|
||||||
|
reduxUser('getUserSSO', [backUrl]);
|
||||||
|
};
|
||||||
|
const initialize = (): void => {
|
||||||
|
if (storeGlobal.globalEnv.env.EnvName === 'prod') {
|
||||||
|
onSubmitSSO();
|
||||||
|
}
|
||||||
|
setIsFirstInitial(true);
|
||||||
|
};
|
||||||
|
/* Hooks */
|
||||||
|
useDidMount(() => {
|
||||||
|
initialize();
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isFirstInitial) return;
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [storeUser.userAccount]);
|
||||||
|
/* Main */
|
||||||
|
return (
|
||||||
|
<div className={classNames(Styles.signInContainer)}>
|
||||||
|
<div className={classNames(Styles.signInPanelContainer)}>
|
||||||
|
<div className={classNames(Styles.signInTitleContainer)}>
|
||||||
|
<Typography className={classNames(Styles.signInTitleMainStyle)} variant="h4" color="textPrimary">
|
||||||
|
{i18n.t('frontend.local.signin.logintitle')}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<div className={classNames(Styles.signInActionContainer)}>
|
||||||
|
<Button
|
||||||
|
className={classNames(Styles.signInActionButtonStyle)}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={onSubmitSSO}
|
||||||
|
>
|
||||||
|
{i18n.t('frontend.global.operation.sso')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Loading typePosition="absolute" typeZIndex={20000} typeIcon="line:fix" isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(SignIn);
|
37
src/css/index.css
Normal file
37
src/css/index.css
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
*,
|
||||||
|
:after,
|
||||||
|
:before {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
--ck-z-default: 100;
|
||||||
|
--ck-z-modal: calc(var(--ck-z-default) + 999);
|
||||||
|
}
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
td:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
}
|
||||||
|
td:last-child {
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
12
src/env/Config/Default.ts
vendored
Normal file
12
src/env/Config/Default.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { EnvConfig } from '../types';
|
||||||
|
|
||||||
|
const config: EnvConfig = {
|
||||||
|
EnvName: 'default',
|
||||||
|
EnvShortName: 'l',
|
||||||
|
EnvUrl: '',
|
||||||
|
APIUrl: 'http://localhost:10230',
|
||||||
|
I18nLocalName: 'default-lang',
|
||||||
|
JwtTokenLocalName: 'default-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
12
src/env/Config/Development.ts
vendored
Normal file
12
src/env/Config/Development.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { EnvConfig } from '../types';
|
||||||
|
|
||||||
|
const config: EnvConfig = {
|
||||||
|
EnvName: 'development',
|
||||||
|
EnvShortName: 'd',
|
||||||
|
EnvUrl: '',
|
||||||
|
APIUrl: 'http://localhost:10230',
|
||||||
|
I18nLocalName: 'development-lang',
|
||||||
|
JwtTokenLocalName: 'development-token',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
57
src/env/index.ts
vendored
Normal file
57
src/env/index.ts
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import Default from './Config/Default';
|
||||||
|
import Development from './Config/Development';
|
||||||
|
import { EnvConfig } from './types';
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
constructor() {
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public env: EnvConfig = Default;
|
||||||
|
|
||||||
|
public replaceCompanyLogo = false;
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
if (process.env.APP_ENV) {
|
||||||
|
this.setEnv(process.env.APP_ENV);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnv(env: string): void {
|
||||||
|
switch (env) {
|
||||||
|
case 'dev':
|
||||||
|
this.env = Development;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.env = Default;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnv(): EnvConfig {
|
||||||
|
return this.env;
|
||||||
|
}
|
||||||
|
|
||||||
|
get HostApiUrl(): string {
|
||||||
|
return `${this.env.APIUrl}/api`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get HostUrl(): string {
|
||||||
|
return this.env.EnvUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
get TokenLocalStorageName(): string {
|
||||||
|
return this.env.JwtTokenLocalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get I18nLocalStorageName(): string {
|
||||||
|
return this.env.I18nLocalName;
|
||||||
|
}
|
||||||
|
|
||||||
|
get EnvName(): string {
|
||||||
|
return this.env.EnvName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Config;
|
8
src/env/types.d.ts
vendored
Normal file
8
src/env/types.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface EnvConfig {
|
||||||
|
readonly EnvName: string;
|
||||||
|
readonly EnvShortName: string;
|
||||||
|
readonly EnvUrl: string;
|
||||||
|
readonly APIUrl: string;
|
||||||
|
readonly I18nLocalName: string;
|
||||||
|
readonly JwtTokenLocalName: string;
|
||||||
|
}
|
5
src/hooks/useDidMount.tsx
Normal file
5
src/hooks/useDidMount.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { useEffect, EffectCallback } from 'react';
|
||||||
|
|
||||||
|
export default function useDidMount(effect: EffectCallback): void {
|
||||||
|
return useEffect(effect, []);
|
||||||
|
}
|
9
src/hooks/useDispatch.tsx
Normal file
9
src/hooks/useDispatch.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { useDispatch as BaseUseDispatch } from 'react-redux';
|
||||||
|
import { Action, AnyAction } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { StoreState } from '@Reducers/_InitializeStore/types';
|
||||||
|
|
||||||
|
export type UseDispatch<T extends Action = AnyAction> = () => ThunkDispatch<StoreState, Record<string, unknown>, T>;
|
||||||
|
const useDispatch: UseDispatch = BaseUseDispatch;
|
||||||
|
|
||||||
|
export default useDispatch;
|
16
src/hooks/useLang.tsx
Normal file
16
src/hooks/useLang.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import I18n from '@Models/Core/I18n';
|
||||||
|
import useMappedState from './useMappedState';
|
||||||
|
|
||||||
|
export interface UseLang {
|
||||||
|
i18n: I18n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述 : 取得 翻譯檔 的 Class Instance
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useLang(): UseLang {
|
||||||
|
const storeGlobal = useMappedState((state) => state.global);
|
||||||
|
return useMemo(() => ({ i18n: storeGlobal.globalI18n }), [storeGlobal.globalLang]);
|
||||||
|
}
|
112
src/hooks/useMappedRoute.tsx
Normal file
112
src/hooks/useMappedRoute.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Route, Redirect, Switch, useRouteMatch,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import LazyComponent from '@Components/Base/Lazy';
|
||||||
|
import BaseRoutes from '@Base/routes';
|
||||||
|
import { RouteItem } from '@Base/routes/types';
|
||||||
|
import useMappedState from './useMappedState';
|
||||||
|
|
||||||
|
type useMappedRouteProps = React.ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述 : 取得某個 Route 底下的 SubRoute
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useMappedRoute(
|
||||||
|
routeLayer: string[],
|
||||||
|
signInRedirect = '',
|
||||||
|
noSignInRedirect = '',
|
||||||
|
): useMappedRouteProps {
|
||||||
|
/* Global & Local State */
|
||||||
|
const routeMatch = useRouteMatch();
|
||||||
|
const storeUser = useMappedState((state) => state.user);
|
||||||
|
/* Data */
|
||||||
|
const ReturnRoutes: RouteItem[] = useMemo(() => {
|
||||||
|
let fullRoutes: RouteItem[] = [];
|
||||||
|
let result: RouteItem[] | null = null;
|
||||||
|
let hadResult = false;
|
||||||
|
if (storeUser.userIsLogin) {
|
||||||
|
Object.values(BaseRoutes.Admin).forEach((route) => {
|
||||||
|
fullRoutes = fullRoutes.concat(route);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.values(BaseRoutes.Guest).forEach((route) => {
|
||||||
|
fullRoutes = fullRoutes.concat(route);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (routeLayer.length === 0) {
|
||||||
|
result = fullRoutes;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < routeLayer.length; i += 1) {
|
||||||
|
if (!hadResult) {
|
||||||
|
const recentRouteObject = fullRoutes.find((route) => route.name === routeLayer[i]);
|
||||||
|
if (recentRouteObject) {
|
||||||
|
result = recentRouteObject.subRoute;
|
||||||
|
hadResult = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hadResult && result) {
|
||||||
|
const recentRouteObject = result.find((route) => route.name === routeLayer[i]);
|
||||||
|
if (recentRouteObject) {
|
||||||
|
result = recentRouteObject.subRoute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [storeUser.userIsLogin]);
|
||||||
|
const ReturnReDirect = useMemo<string>(() => {
|
||||||
|
if (storeUser.userIsLogin) {
|
||||||
|
if (ReturnRoutes.length > 0) {
|
||||||
|
return ReturnRoutes[0].route;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ReturnRoutes.length > 0) {
|
||||||
|
return ReturnRoutes[0].route;
|
||||||
|
}
|
||||||
|
return '/';
|
||||||
|
}, [storeUser.userIsLogin, signInRedirect, noSignInRedirect]);
|
||||||
|
/* View */
|
||||||
|
const RenderRoutes = useMemo(
|
||||||
|
() => (
|
||||||
|
<Switch>
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path={`${routeMatch.url.replace('/', '') === '' ? '/' : `/${routeMatch.url.replace('/', '')}`}`}
|
||||||
|
render={() => (
|
||||||
|
<Redirect
|
||||||
|
to={`${
|
||||||
|
routeMatch.url.replace('/', '') === '' ? '' : `/${routeMatch.url.replace('/', '')}`
|
||||||
|
}${ReturnReDirect}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{ReturnRoutes.map((route) => (
|
||||||
|
<Route
|
||||||
|
key={route.route}
|
||||||
|
exact={route.exact}
|
||||||
|
path={`${routeMatch.path.replace('/', '') === '' ? '' : `/${routeMatch.path.replace('/', '')}`}${
|
||||||
|
route.route
|
||||||
|
}`}
|
||||||
|
render={() => (
|
||||||
|
<LazyComponent
|
||||||
|
componentImport={route.component}
|
||||||
|
componentChunkName={`${route.name}Chunk`}
|
||||||
|
componentProps={{}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Redirect
|
||||||
|
to={`${routeMatch.url.replace('/', '') === '' ? '' : `/${routeMatch.url.replace('/', '')}`}${ReturnReDirect}`}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
),
|
||||||
|
[storeUser.userIsLogin, routeMatch, ReturnReDirect],
|
||||||
|
);
|
||||||
|
/* Main */
|
||||||
|
return <>{RenderRoutes}</>;
|
||||||
|
}
|
13
src/hooks/useMappedState.tsx
Normal file
13
src/hooks/useMappedState.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { StoreState } from '@Reducers/_InitializeStore/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述 : 取得 Redux 內的值
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useMappedState<T>(
|
||||||
|
mapState: (state: StoreState) => T,
|
||||||
|
equalityFn?: (left: T, right: T) => boolean,
|
||||||
|
): T {
|
||||||
|
return useSelector<StoreState, T>(mapState, equalityFn);
|
||||||
|
}
|
55
src/hooks/useMessage.tsx
Normal file
55
src/hooks/useMessage.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { APIError } from '@Models/GeneralTypes';
|
||||||
|
import {
|
||||||
|
postMessageDialog,
|
||||||
|
postMessageConfirm,
|
||||||
|
removeMessageDialog,
|
||||||
|
removeMessageConfirm,
|
||||||
|
} from '@Reducers/message/actions';
|
||||||
|
import { MessageDialogObject, MessageConfirmObject } from '@Models/Redux/Message/types';
|
||||||
|
import { errorCatch } from '@Reducers/_Capture/errorCapture';
|
||||||
|
import useDispatch from './useDispatch';
|
||||||
|
|
||||||
|
interface useMessageDefine {
|
||||||
|
dialog: (dialog: MessageDialogObject) => void;
|
||||||
|
confirm: (confirm: MessageConfirmObject) => void;
|
||||||
|
removeDialog: () => void;
|
||||||
|
removeConfirm: () => void;
|
||||||
|
error: (error: APIError) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述 : 取得 Redux Action 裡面關於 Message 的 Action
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useMessage(): useMessageDefine {
|
||||||
|
/* Global & Local State */
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
/* Functions */
|
||||||
|
const apiCallPostMessageDialog = (dialog: MessageDialogObject): void => {
|
||||||
|
dispatch(postMessageDialog(dialog));
|
||||||
|
};
|
||||||
|
const apiCallPostMessageConfirm = (confirm: MessageConfirmObject): void => {
|
||||||
|
dispatch(postMessageConfirm(confirm));
|
||||||
|
};
|
||||||
|
const apiCallRemoveMessageDialog = (): void => {
|
||||||
|
dispatch(removeMessageDialog());
|
||||||
|
};
|
||||||
|
const apiCallRemoveMessageConfirm = (): void => {
|
||||||
|
dispatch(removeMessageConfirm());
|
||||||
|
};
|
||||||
|
const apiCallError = (error): void => {
|
||||||
|
dispatch(errorCatch(error));
|
||||||
|
};
|
||||||
|
/* Main */
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
dialog: apiCallPostMessageDialog,
|
||||||
|
confirm: apiCallPostMessageConfirm,
|
||||||
|
removeDialog: apiCallRemoveMessageDialog,
|
||||||
|
removeConfirm: apiCallRemoveMessageConfirm,
|
||||||
|
error: apiCallError,
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
65
src/hooks/useReduxApi.tsx
Normal file
65
src/hooks/useReduxApi.tsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
import { StoreState } from '@Reducers/_InitializeStore/types';
|
||||||
|
import useDispatch from './useDispatch';
|
||||||
|
|
||||||
|
type useReduxAPICallBack = (action: string, props: Array<unknown>) => void;
|
||||||
|
|
||||||
|
type actionType =
|
||||||
|
| 'user'
|
||||||
|
| 'global'
|
||||||
|
| 'message';
|
||||||
|
|
||||||
|
interface ReduxAction {
|
||||||
|
[key: string]: (...props: Array<unknown>) => ThunkAction<Promise<void>, StoreState, unknown, { type: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReduxActionModule {
|
||||||
|
[key: string]: ReduxAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_ACTIONS: actionType[] = [
|
||||||
|
'user',
|
||||||
|
'global',
|
||||||
|
'message',
|
||||||
|
];
|
||||||
|
|
||||||
|
const LOAD_MODULE: ReduxActionModule = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 描述 : 取得指定的 Redux Action
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function useReduxAPI(actionType: actionType): useReduxAPICallBack {
|
||||||
|
/* Global & Local State */
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
/* Main */
|
||||||
|
return useCallback(
|
||||||
|
(action: string, props: Array<unknown>) => {
|
||||||
|
const isInclude = VALID_ACTIONS.includes(actionType);
|
||||||
|
let reduxType: ReduxAction = {};
|
||||||
|
if (isInclude) {
|
||||||
|
if (LOAD_MODULE[actionType]) {
|
||||||
|
reduxType = LOAD_MODULE[actionType];
|
||||||
|
} else {
|
||||||
|
LOAD_MODULE[actionType] = require(`@Reducers/${actionType}/actions`);
|
||||||
|
reduxType = LOAD_MODULE[actionType];
|
||||||
|
}
|
||||||
|
if (reduxType) {
|
||||||
|
const reduxAction = reduxType[action];
|
||||||
|
if (reduxAction) {
|
||||||
|
dispatch(reduxAction(...props));
|
||||||
|
} else {
|
||||||
|
console.warn(`Can't find the action ::: ${action}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Can't load module ::: ${reduxType}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`Can't find the action type ::: ${actionType}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[actionType],
|
||||||
|
);
|
||||||
|
}
|
19
src/index.tsx
Normal file
19
src/index.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import '@CSS/index.css';
|
||||||
|
import '@Plugin/index';
|
||||||
|
|
||||||
|
async function renderMainApp(): Promise<void> {
|
||||||
|
const mainAppProvider = (await import('@Providers/MainAppProvider')).default;
|
||||||
|
const Provider = mainAppProvider({
|
||||||
|
Router: BrowserRouter,
|
||||||
|
appKey: 520,
|
||||||
|
routerProps: {
|
||||||
|
basename: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ReactDOM.render(<Provider />, document.getElementById('root'));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMainApp();
|
3
src/langs/data/en.ts
Normal file
3
src/langs/data/en.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const lang = {};
|
||||||
|
|
||||||
|
export default lang;
|
23
src/langs/data/tw.ts
Normal file
23
src/langs/data/tw.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const lang = {
|
||||||
|
'frontend.global.operation.confirm': '確認',
|
||||||
|
'frontend.global.operation.ok': '確定',
|
||||||
|
'frontend.global.operation.cancel': '取消',
|
||||||
|
'frontend.global.operation.signout': '登出',
|
||||||
|
'frontend.global.operation.sso': '單一登入',
|
||||||
|
|
||||||
|
'frontend.global.property.routehome': '首頁',
|
||||||
|
|
||||||
|
'frontend.local.signin.logintitle': 'KeyCloak Demo Server',
|
||||||
|
|
||||||
|
'frontend.global.error.9999': '未知的錯誤',
|
||||||
|
'frontend.global.error.0000': '錯誤',
|
||||||
|
'frontend.global.error.1001': '建立',
|
||||||
|
'frontend.global.error.1002': '已接收',
|
||||||
|
'frontend.global.error.1003': '資料格式錯誤',
|
||||||
|
'frontend.global.error.1004': '尚未登入',
|
||||||
|
'frontend.global.error.1005': '無權限可瀏覽',
|
||||||
|
'frontend.global.error.1006': '查無此資料',
|
||||||
|
'frontend.global.error.1007': '伺服器內部錯誤,請尋求客服的協助',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default lang;
|
1
src/langs/list/index.ts
Normal file
1
src/langs/list/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default ['zh-TW', 'en'];
|
75
src/models/Core/I18n/index.ts
Normal file
75
src/models/Core/I18n/index.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import Immerable from '@Models/GeneralImmer';
|
||||||
|
import tw from '@Langs/data/tw';
|
||||||
|
import en from '@Langs/data/en';
|
||||||
|
import { LangObject, LangList } from './types';
|
||||||
|
|
||||||
|
const LANGUAGE_BASE_LIST = [
|
||||||
|
{
|
||||||
|
lang: 'frontend.global.language.en',
|
||||||
|
value: 'en',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lang: 'frontend.global.language.tw',
|
||||||
|
value: 'tw',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
class I18n extends Immerable {
|
||||||
|
public lang: string;
|
||||||
|
|
||||||
|
public langDefault: LangObject;
|
||||||
|
|
||||||
|
public langList: LangList;
|
||||||
|
|
||||||
|
public langBase: LangObject;
|
||||||
|
|
||||||
|
constructor(lang: string) {
|
||||||
|
super();
|
||||||
|
this.lang = lang;
|
||||||
|
this.langDefault = en;
|
||||||
|
this.langList = LANGUAGE_BASE_LIST;
|
||||||
|
this.langBase = en;
|
||||||
|
this.switchLanguage(this.lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
t(key: string): string {
|
||||||
|
let text = this.langBase[key];
|
||||||
|
if (!text) {
|
||||||
|
text = this.langDefault[key] || '';
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchLanguage(language: string): void {
|
||||||
|
switch (language) {
|
||||||
|
case 'en':
|
||||||
|
this.langBase = en;
|
||||||
|
break;
|
||||||
|
case 'tw':
|
||||||
|
this.langBase = tw;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.langBase = en;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSystemLang = (): string => {
|
||||||
|
const lang = navigator.language.toLowerCase();
|
||||||
|
switch (lang) {
|
||||||
|
case 'en':
|
||||||
|
return 'en';
|
||||||
|
case 'tw':
|
||||||
|
case 'zh-tw':
|
||||||
|
return 'tw';
|
||||||
|
default:
|
||||||
|
return 'en';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
get languages(): LangList {
|
||||||
|
return this.langList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default I18n;
|
10
src/models/Core/I18n/types.d.ts
vendored
Normal file
10
src/models/Core/I18n/types.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface LangObject {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LangListItem {
|
||||||
|
lang: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LangList = LangListItem[];
|
5
src/models/GeneralImmer.ts
Normal file
5
src/models/GeneralImmer.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { immerable } from 'immer';
|
||||||
|
|
||||||
|
export default class Immerable {
|
||||||
|
protected [immerable] = true;
|
||||||
|
}
|
40
src/models/GeneralTypes.ts
Normal file
40
src/models/GeneralTypes.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
export interface Pager {
|
||||||
|
page: number;
|
||||||
|
count: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
export interface TimeStamp {
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
export interface Range {
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
}
|
||||||
|
export interface Condition {
|
||||||
|
page: number;
|
||||||
|
}
|
||||||
|
export interface Attachment {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
file?: File;
|
||||||
|
old?: boolean;
|
||||||
|
}
|
||||||
|
export interface AttachmentInDB {
|
||||||
|
path: string;
|
||||||
|
filename: string;
|
||||||
|
url: string;
|
||||||
|
file?: File;
|
||||||
|
old?: boolean;
|
||||||
|
}
|
||||||
|
export interface ValidFileFormat {
|
||||||
|
type: string;
|
||||||
|
display: string;
|
||||||
|
}
|
||||||
|
export interface APIError {
|
||||||
|
message: string;
|
||||||
|
code: number;
|
||||||
|
errorStack: string;
|
||||||
|
errorMessage: string;
|
||||||
|
}
|
||||||
|
export type Status = '建立通知' | '不通知' | '重複' | '不適用' | '未查看' | '已查看';
|
53
src/models/Redux/Global/index.ts
Normal file
53
src/models/Redux/Global/index.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import Immerable from '@Models/GeneralImmer';
|
||||||
|
import I18n from '@Models/Core/I18n';
|
||||||
|
import { MiddleWareObject, MiddleWareAPI, MiddleWareEnv } from './types';
|
||||||
|
|
||||||
|
class Global extends Immerable {
|
||||||
|
public globalLoading: boolean;
|
||||||
|
|
||||||
|
public globalLang: string;
|
||||||
|
|
||||||
|
public globalSideBar: boolean;
|
||||||
|
|
||||||
|
public globalSideBarStatic: boolean;
|
||||||
|
|
||||||
|
public globalAPI: MiddleWareAPI;
|
||||||
|
|
||||||
|
public globalEnv: MiddleWareEnv;
|
||||||
|
|
||||||
|
public globalI18n: I18n;
|
||||||
|
|
||||||
|
public constructor(middleware: MiddleWareObject) {
|
||||||
|
super();
|
||||||
|
this.globalLoading = false;
|
||||||
|
this.globalLang = 'tw';
|
||||||
|
this.globalSideBar = false;
|
||||||
|
this.globalSideBarStatic = true;
|
||||||
|
this.globalAPI = middleware.api;
|
||||||
|
this.globalEnv = middleware.env;
|
||||||
|
this.globalI18n = new I18n(this.globalLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
this.globalLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateGlobalLoading(newGlobalLoading: boolean): void {
|
||||||
|
this.globalLoading = newGlobalLoading;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateGlobalLang(newGlobalLang: string): void {
|
||||||
|
this.globalLang = newGlobalLang;
|
||||||
|
this.globalI18n.switchLanguage(newGlobalLang);
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateGlobalSideBar(newGlobalSideBarState: boolean): void {
|
||||||
|
this.globalSideBar = newGlobalSideBarState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateGlobalSideBarStatic(newGlobalSideBarStatic: boolean): void {
|
||||||
|
this.globalSideBarStatic = newGlobalSideBarStatic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Global;
|
10
src/models/Redux/Global/types.ts
Normal file
10
src/models/Redux/Global/types.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import API from '@API/index';
|
||||||
|
import Env from '@Env/index';
|
||||||
|
|
||||||
|
import { MiddleWare } from '@Reducers/_initializeMiddleware/types';
|
||||||
|
|
||||||
|
export type MiddleWareObject = MiddleWare;
|
||||||
|
|
||||||
|
export type MiddleWareAPI = API;
|
||||||
|
|
||||||
|
export type MiddleWareEnv = Env;
|
61
src/models/Redux/Message/index.ts
Normal file
61
src/models/Redux/Message/index.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import Immerable from '@Models/GeneralImmer';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import {
|
||||||
|
MessageDialogObject,
|
||||||
|
MessageDialogList,
|
||||||
|
MessageConfirmObject,
|
||||||
|
MessageConfirmList,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
class Message extends Immerable {
|
||||||
|
public messageDialogList: MessageDialogList;
|
||||||
|
|
||||||
|
public messageConfirmList: MessageConfirmList;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super();
|
||||||
|
this.messageDialogList = {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
this.messageConfirmList = {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
this.messageDialogList = {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
this.messageConfirmList = {
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateMessageDialogList(newMessageDialogObject: MessageDialogObject): void {
|
||||||
|
const newCloneMessageDialogObject = cloneDeep(newMessageDialogObject);
|
||||||
|
const newCloneMessageDialogList = cloneDeep(this.messageDialogList);
|
||||||
|
newCloneMessageDialogList.list.push(newCloneMessageDialogObject);
|
||||||
|
this.messageDialogList = newCloneMessageDialogList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeMessageDialog(): void {
|
||||||
|
const newCloneMessageDialogList = cloneDeep(this.messageDialogList.list);
|
||||||
|
const newSliceDialogList = newCloneMessageDialogList.slice(1);
|
||||||
|
this.messageDialogList.list = newSliceDialogList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateMessageConfirmList(newMessageConfirmObject: MessageConfirmObject): void {
|
||||||
|
const newCloneMessageConfirmObject = cloneDeep(newMessageConfirmObject);
|
||||||
|
const newCloneMessageConfirmList = cloneDeep(this.messageConfirmList);
|
||||||
|
newCloneMessageConfirmList.list.push(newCloneMessageConfirmObject);
|
||||||
|
this.messageConfirmList = newCloneMessageConfirmList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeMessageConfirm(): void {
|
||||||
|
const newCloneMessageConfirmList = cloneDeep(this.messageConfirmList.list);
|
||||||
|
const newSliceConfirmList = newCloneMessageConfirmList.slice(1);
|
||||||
|
this.messageConfirmList.list = newSliceConfirmList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Message;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user