[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