[feat] Initial code

This commit is contained in:
JasonWu 2021-09-06 18:00:03 +08:00
commit 7dc01d5480
138 changed files with 48897 additions and 0 deletions

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
/config
/scripts
/libs
/src/services/Scripts/ElementWorkers/*
/.eslintrc.js

85
.eslintrc.js Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
# Changelog
## 1.0.0

21
Dockerfile Normal file
View 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
View 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
View 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
View 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 were 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
View 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;

View 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';
},
};

View 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
View 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
View 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
View 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
View 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,
};
};

View 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 wont 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
View 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

File diff suppressed because it is too large Load Diff

219
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

20
public/index.html Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

212
scripts/build.js Normal file
View 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
View 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
View 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);

View 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;

View 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
View File

@ -0,0 +1,3 @@
export interface HeaderContent {
Authorization?: string;
}

49
src/api/index.ts Normal file
View 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
View File

@ -0,0 +1,3 @@
export interface APIParams {
host: string;
}

View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 B

View File

@ -0,0 +1 @@
export const ENTER_KEY_CODE = 13;

32
src/base/routes/Admin.ts Normal file
View 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
View 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
View 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
View 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[];
}

View File

@ -0,0 +1 @@
export const TIMEZONE_TAIPEI = 'Asia/Taipei';

3
src/base/url/Admin.ts Normal file
View 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
View 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
View File

@ -0,0 +1,7 @@
import * as GUEST from './Guest';
import * as ADMIN from './Admin';
export default {
GUEST,
ADMIN,
};

View File

@ -0,0 +1,6 @@
/* Component Style */
:local(.appContainer) {
width: 100%;
height: 100%;
position: relative;
}

View 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
View 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;
}

View 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;
}

View 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);

View 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
View 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;
}

View 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;
}

View 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);

View 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;
}

View 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;
}

View 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
View 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;
}

View 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;
}

View 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);

View File

@ -0,0 +1,6 @@
import { MessageConfirmObject } from '@Models/Redux/Message/types';
export interface ConfirmItemObject {
confirm: MessageConfirmObject;
requestRemoveConfirm: () => void;
}

View 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;
}

View 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);

View File

@ -0,0 +1,6 @@
import { MessageDialogObject } from '@Models/Redux/Message/types';
export interface DialogItemObject {
dialog: MessageDialogObject;
requestRemoveDialog: () => void;
}

View 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);

View 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;
}

View 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);

View 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;
}

View 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);

View File

@ -0,0 +1,6 @@
/* Component Style */
:local(.homeContainer) {
width: 100%;
height: 100%;
position: relative;
}

View 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);

View File

@ -0,0 +1,10 @@
/* Component Style */
:local(.oAuthContainer) {
width: 100%;
height: 100%;
position: fixed;
top: 0px;
left: 0px;
background: white;
z-index: 100;
}

View 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
View 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 };

View 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;
}
}

View 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
View 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
View 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
View 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
View 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
View 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;
}

View File

@ -0,0 +1,5 @@
import { useEffect, EffectCallback } from 'react';
export default function useDidMount(effect: EffectCallback): void {
return useEffect(effect, []);
}

View 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
View 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]);
}

View 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}</>;
}

View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
const lang = {};
export default lang;

23
src/langs/data/tw.ts Normal file
View 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
View File

@ -0,0 +1 @@
export default ['zh-TW', 'en'];

View 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
View File

@ -0,0 +1,10 @@
export interface LangObject {
[key: string]: string;
}
export interface LangListItem {
lang: string;
value: string;
}
export type LangList = LangListItem[];

View File

@ -0,0 +1,5 @@
import { immerable } from 'immer';
export default class Immerable {
protected [immerable] = true;
}

View 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 = '建立通知' | '不通知' | '重複' | '不適用' | '未查看' | '已查看';

View 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;

View 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;

View 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