├── .env.development ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── config ├── @types │ └── index.d.ts ├── env.js ├── jest │ ├── cssMock.js │ ├── routeMock.js │ └── svgrMock.js └── paths.js ├── docs ├── README.md ├── index.html └── style.css ├── package.json ├── pm2 └── prod.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── static │ └── images │ ├── banner.png │ ├── icon │ ├── arrow-down.svg │ ├── en.svg │ ├── github.svg │ ├── linkedin.svg │ ├── stack-overflow.svg │ ├── vi.svg │ └── view-details.svg │ ├── login-avatar.png │ └── webpack.png ├── server.js ├── src ├── App.test.tsx ├── App.tsx ├── __snapshots__ │ └── App.test.tsx.snap ├── apis │ ├── auth.ts │ └── todo.ts ├── components │ ├── button │ │ ├── Button.test.tsx │ │ ├── index.tsx │ │ └── style.scss │ ├── not-found │ │ ├── index.tsx │ │ └── style.scss │ ├── page-loading │ │ ├── index.tsx │ │ └── style.scss │ ├── select │ │ ├── index.tsx │ │ └── style.scss │ └── spinner │ │ ├── index.tsx │ │ └── style.scss ├── constants │ └── env.ts ├── features │ ├── core │ │ ├── components │ │ │ └── login │ │ │ │ ├── index.tsx │ │ │ │ └── style.scss │ │ └── route.ts │ └── home │ │ ├── components │ │ ├── Home.tsx │ │ └── tag │ │ │ ├── index.tsx │ │ │ └── style.scss │ │ ├── index.tsx │ │ ├── route.ts │ │ ├── style.scss │ │ └── types │ │ └── index.ts ├── helpers │ ├── axios.ts │ ├── local-storage.ts │ ├── router.ts │ └── toast.ts ├── hooks │ ├── useDidMountEffect.tsx │ ├── useOutsideClick.tsx │ ├── useSafeState.tsx │ ├── useShallowEqualSelector.tsx │ └── useWindowSize.tsx ├── index.tsx ├── jest.config.js ├── layouts │ ├── Auth.tsx │ ├── footer │ │ ├── index.tsx │ │ └── style.scss │ ├── header │ │ ├── index.tsx │ │ └── style.scss │ ├── index.tsx │ └── main │ │ ├── index.tsx │ │ └── style.scss ├── locales │ ├── i18n.ts │ ├── languages.ts │ └── resources │ │ ├── footer.json │ │ ├── home.json │ │ └── index.ts ├── router │ └── index.ts ├── services │ ├── axios-base.ts │ └── http-request.ts ├── setupTests.ts ├── store │ ├── index.ts │ └── slices │ │ ├── appSlice.ts │ │ ├── authSlice.ts │ │ └── layoutSlice.ts ├── styles │ ├── App.scss │ ├── _color.scss │ ├── _container.scss │ ├── _font.scss │ ├── _mixin.scss │ ├── _reset.scss │ ├── _size.scss │ ├── _utils.scss │ └── typography.scss └── utils │ └── index.ts ├── tsconfig.json ├── webpack ├── webpack.common.js ├── webpack.dev.js └── webpack.prod.js └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | API_SERVER_URL=http://localhost:3000 3 | PORT=3000 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:react/recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | parser: '@typescript-eslint/parser', 14 | parserOptions: { 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | ecmaVersion: 12, 19 | sourceType: 'module', 20 | }, 21 | plugins: ['react', 'react-hooks', '@typescript-eslint'], 22 | rules: { 23 | 'react/jsx-closing-bracket-location': 'warn', 24 | 'react/jsx-tag-spacing': 'off', 25 | 'array-bracket-spacing': [0, 'never'], 26 | 'react/prop-types': 'off', 27 | 'prefer-const': 'warn', 28 | 'jsx-quotes': ['error', 'prefer-double'], 29 | 'no-console': 'off', 30 | 'react-hooks/rules-of-hooks': 'error', 31 | 'react-hooks/exhaustive-deps': 'warn', 32 | 'no-useless-escape': 'off', 33 | // unknown is I don't know; any is I don't care 34 | '@typescript-eslint/no-explicit-any': ['off', { ignoreRestArgs: true }], 35 | '@typescript-eslint/no-var-requires': 0, 36 | '@typescript-eslint/ban-types': ['off'], 37 | }, 38 | // Fix warning https://github.com/yannickcr/eslint-plugin-react#configuration 39 | settings: { 40 | react: { 41 | createClass: 'createReactClass', // Regex for Component Factory to use, 42 | // default to "createReactClass" 43 | pragma: 'React', // Pragma to use, default to "React" 44 | fragment: 'Fragment', // Fragment to use (may be a property of ), default to "Fragment" 45 | version: 'detect', // React version. "detect" automatically picks the version you have installed. 46 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value. 47 | // default to latest and warns if missing 48 | // It will default to "detect" in the future 49 | flowVersion: '0.53', // Flow version 50 | }, 51 | propWrapperFunctions: [ 52 | // The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped. 53 | 'forbidExtraProps', 54 | { property: 'freeze', object: 'Object' }, 55 | { property: 'myFavoriteWrapper' }, 56 | ], 57 | linkComponents: [ 58 | // Components used as alternatives to for linking, eg. 59 | 'Hyperlink', 60 | { name: 'Link', linkAttribute: 'to' }, 61 | ], 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Frontend 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # configuration 5 | .env 6 | .husky 7 | 8 | # build 9 | /build 10 | /dist 11 | 12 | # dependencies 13 | /node_modules 14 | 15 | # misc 16 | .DS_Store 17 | .DS_STORE 18 | # .env.local 19 | # .env.development.local 20 | # .env.test.local 21 | # .env.production.local 22 | 23 | # Log 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | # package-lock.json 30 | 31 | 32 | # dotenv environment variables file 33 | .env 34 | .env.production 35 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | package-lock.json 4 | yarn.lock -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "arrowParens": "avoid" 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "eslintignore", 4 | "huskyinstall", 5 | "prettierignore", 6 | "toastify", 7 | "todos" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Aldenn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | Git issue 4 | Git forks 5 | Git star 6 | Git MIT 7 |

8 | react typescript webpack starter 9 |
10 |
11 | 12 |

Start your react typescript project with manual webpack config in seconds

13 | 14 |
Flexible to control webpack, easy to deploy
15 |
Keywords: React Starter, Webpack, Typescript, React.js, Redux, Babel, jest, react-router, sass, redux-thunk, pm2
16 | 17 |

Created with by 👻 Aldenn

18 | 19 |
20 | 21 | # Overview 22 | 23 | React-Typescript-Webpack was config with React, Typescript and Webpack without CRA. Faster to start your next react project. 24 | 25 | --- 26 | 27 | # Documentation 28 | 29 | Full documentation [here](https://thaind97git.github.io/react-typescript-webpack/) 30 | 31 | ## License 32 | 33 | This project is licensed under the MIT license, Copyright (c) 2021 Aldenn. 34 | For more information see `LICENSE.md`. 35 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: ['@babel/preset-env', '@babel/preset-react'], 5 | plugins: ['@babel/plugin-transform-runtime'], 6 | sourceType: 'unambiguous', 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /config/@types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import React from 'react'; 3 | const SVG: React.VFC>; 4 | export default SVG; 5 | } 6 | 7 | declare module '*.jpg' { 8 | const content: string; 9 | export default content; 10 | } 11 | 12 | declare module '*.png' { 13 | const content: string; 14 | export default content; 15 | } 16 | 17 | declare module '*.json' { 18 | const content: any; 19 | export default content; 20 | } 21 | 22 | declare module '*route.ts' { 23 | const content: Array; 24 | export default content; 25 | } 26 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // @remove-on-eject-begin 2 | /** 3 | * Copyright (c) 2015-present, Facebook, Inc. 4 | * 5 | * This source code is licensed under the MIT license found in the 6 | * LICENSE file in the root directory of this source tree. 7 | */ 8 | // @remove-on-eject-end 9 | 'use strict'; 10 | 11 | const fs = require('fs'); 12 | const path = require('path'); 13 | const paths = require('./paths'); 14 | const PACKAGE = require(paths.appPackageJson); 15 | 16 | // Make sure that including paths.js after env.js will read .env variables. 17 | delete require.cache[require.resolve('./paths')]; 18 | 19 | const NODE_ENV = process.env.NODE_ENV; 20 | if (!NODE_ENV) { 21 | throw new Error( 22 | 'The NODE_ENV environment variable is required but was not specified.', 23 | ); 24 | } 25 | 26 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 27 | const dotenvFiles = [ 28 | // `${paths.dotenv}.${NODE_ENV}.local`, 29 | // Don't include `.env.local` for `test` environment 30 | // since normally you expect tests to produce the same 31 | // results for everyone 32 | // NODE_ENV !== 'test' && `${paths.dotenv}.local`, 33 | `${paths.dotenv}`, 34 | // paths.dotenv, 35 | ].filter(Boolean); 36 | 37 | // Load environment variables from .env* files. Suppress warnings using silent 38 | // if this file is missing. dotenv will never modify any environment variables 39 | // that have already been set. Variable expansion is supported in .env files. 40 | // https://github.com/motdotla/dotenv 41 | // https://github.com/motdotla/dotenv-expand 42 | dotenvFiles.forEach(dotenvFile => { 43 | if (fs.existsSync(dotenvFile)) { 44 | require('dotenv-expand')( 45 | require('dotenv').config({ 46 | path: dotenvFile, 47 | }), 48 | ); 49 | } 50 | }); 51 | 52 | // We support resolving modules according to `NODE_PATH`. 53 | // This lets you use absolute paths in imports inside large monorepos: 54 | // https://github.com/facebook/create-react-app/issues/253. 55 | // It works similar to `NODE_PATH` in Node itself: 56 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 57 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 58 | // Otherwise, we risk importing Node.js core modules into an app instead of webpack shims. 59 | // https://github.com/facebook/create-react-app/issues/1023#issuecomment-265344421 60 | // We also resolve them to make sure all tools using them work consistently. 61 | const appDirectory = fs.realpathSync(process.cwd()); 62 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 63 | .split(path.delimiter) 64 | .filter(folder => folder && !path.isAbsolute(folder)) 65 | .map(folder => path.resolve(appDirectory, folder)) 66 | .join(path.delimiter); 67 | 68 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 69 | // injected into the application via DefinePlugin in webpack configuration. 70 | const REACT_APP = /^/i; 71 | 72 | function getClientEnvironment(publicUrl) { 73 | const raw = Object.keys(process.env) 74 | .filter(key => REACT_APP.test(key)) 75 | .reduce( 76 | (env, key) => { 77 | env[key] = process.env[key]; 78 | return env; 79 | }, 80 | { 81 | // Useful for determining whether we’re running in production mode. 82 | // Most importantly, it switches React into the correct mode. 83 | NODE_ENV: process.env.NODE_ENV || 'development', 84 | // Useful for resolving the correct path to static assets in `public`. 85 | // For example, . 86 | // This should only be used as an escape hatch. Normally you would put 87 | // images into the `src` and `import` them in code to get their paths. 88 | PUBLIC_URL: publicUrl, 89 | VERSION: PACKAGE.version, 90 | }, 91 | ); 92 | // Stringify all values so we can feed into webpack DefinePlugin 93 | const stringified = { 94 | 'process.env': Object.keys(raw).reduce((env, key) => { 95 | env[key] = JSON.stringify(raw[key]); 96 | return env; 97 | }, {}), 98 | }; 99 | 100 | return { raw, stringified }; 101 | } 102 | 103 | module.exports = getClientEnvironment; 104 | -------------------------------------------------------------------------------- /config/jest/cssMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /config/jest/routeMock.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('../paths'); 3 | 4 | const { appSrc } = path; 5 | const featuresDir = `${appSrc}/features`; 6 | 7 | const routeFileName = 'route.ts'; 8 | 9 | const RouteModules = []; 10 | function readFolder(dir) { 11 | const files = fs.readdirSync(dir); 12 | files.forEach(file => { 13 | const filePath = `${dir}/${file}`; 14 | const stat = fs.statSync(filePath); 15 | if (stat.isDirectory()) { 16 | readFolder(filePath); 17 | } else { 18 | if (file === routeFileName) { 19 | const route = require(filePath); 20 | RouteModules.push(route); 21 | } 22 | } 23 | }); 24 | } 25 | 26 | readFolder(featuresDir); 27 | 28 | const appRoutes = RouteModules.reduce((prev, module) => { 29 | prev.push(...module.default); 30 | return prev; 31 | }, []); 32 | 33 | module.exports = appRoutes; 34 | -------------------------------------------------------------------------------- /config/jest/svgrMock.js: -------------------------------------------------------------------------------- 1 | export default 'SvgrURL'; 2 | export const ReactComponent = 'div'; 3 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | // const getPublicUrlOrPath = require('react-dev-utils/getPublicUrlOrPath'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 13 | // "public path" at which the app is served. 14 | // webpack needs to know it to put the right 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Mono|Source+Sans+Pro:300,400,600'); 2 | * { 3 | -webkit-font-smoothing: antialiased; 4 | -webkit-overflow-scrolling: touch; 5 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 6 | -webkit-text-size-adjust: none; 7 | -webkit-touch-callout: none; 8 | box-sizing: border-box; 9 | } 10 | body:not(.ready) { 11 | overflow: hidden; 12 | } 13 | body:not(.ready) .app-nav, 14 | body:not(.ready) > nav, 15 | body:not(.ready) [data-cloak] { 16 | display: none; 17 | } 18 | div#app { 19 | font-size: 30px; 20 | font-weight: lighter; 21 | margin: 40vh auto; 22 | text-align: center; 23 | } 24 | div#app:empty:before { 25 | content: 'Loading...'; 26 | } 27 | .emoji { 28 | height: 1.2rem; 29 | vertical-align: middle; 30 | } 31 | .progress { 32 | background-color: var(--theme-color, #048fd6); 33 | height: 2px; 34 | left: 0; 35 | position: fixed; 36 | right: 0; 37 | top: 0; 38 | transition: width 0.2s, opacity 0.4s; 39 | width: 0; 40 | z-index: 999999; 41 | } 42 | .search .search-keyword, 43 | .search a:hover { 44 | color: var(--theme-color, #048fd6); 45 | } 46 | .search .search-keyword { 47 | font-style: normal; 48 | font-weight: 700; 49 | } 50 | body, 51 | html { 52 | height: 100%; 53 | } 54 | body { 55 | -moz-osx-font-smoothing: grayscale; 56 | -webkit-font-smoothing: antialiased; 57 | color: #34495e; 58 | font-family: Source Sans Pro, Helvetica Neue, Arial, sans-serif; 59 | font-size: 15px; 60 | letter-spacing: 0; 61 | margin: 0; 62 | overflow-x: hidden; 63 | } 64 | img { 65 | max-width: 100%; 66 | } 67 | a[disabled] { 68 | cursor: not-allowed; 69 | opacity: 0.6; 70 | } 71 | kbd { 72 | border: 1px solid #ccc; 73 | border-radius: 3px; 74 | display: inline-block; 75 | font-size: 12px !important; 76 | line-height: 12px; 77 | margin-bottom: 3px; 78 | padding: 3px 5px; 79 | vertical-align: middle; 80 | } 81 | li input[type='checkbox'] { 82 | margin: 0 0.2em 0.25em 0; 83 | vertical-align: middle; 84 | } 85 | .app-nav { 86 | margin: 25px 60px 0 0; 87 | position: absolute; 88 | right: 0; 89 | text-align: right; 90 | z-index: 10; 91 | } 92 | .app-nav.no-badge { 93 | margin-right: 25px; 94 | } 95 | .app-nav p { 96 | margin: 0; 97 | } 98 | .app-nav > a { 99 | margin: 0 1rem; 100 | padding: 5px 0; 101 | } 102 | .app-nav li, 103 | .app-nav ul { 104 | display: inline-block; 105 | list-style: none; 106 | margin: 0; 107 | } 108 | .app-nav a { 109 | color: inherit; 110 | font-size: 16px; 111 | text-decoration: none; 112 | transition: color 0.3s; 113 | } 114 | .app-nav a.active, 115 | .app-nav a:hover { 116 | color: var(--theme-color, #048fd6); 117 | } 118 | .app-nav a.active { 119 | border-bottom: 2px solid var(--theme-color, #048fd6); 120 | } 121 | .app-nav li { 122 | display: inline-block; 123 | margin: 0 1rem; 124 | padding: 5px 0; 125 | position: relative; 126 | cursor: pointer; 127 | } 128 | .app-nav li ul { 129 | background-color: #fff; 130 | border: 1px solid; 131 | border-color: #ddd #ddd #ccc; 132 | border-radius: 4px; 133 | box-sizing: border-box; 134 | display: none; 135 | max-height: calc(100vh - 61px); 136 | overflow-y: auto; 137 | padding: 10px 0; 138 | position: absolute; 139 | right: -15px; 140 | text-align: left; 141 | top: 100%; 142 | white-space: nowrap; 143 | } 144 | .app-nav li ul li { 145 | display: block; 146 | font-size: 14px; 147 | line-height: 1rem; 148 | margin: 8px 14px; 149 | white-space: nowrap; 150 | } 151 | .app-nav li ul a { 152 | display: block; 153 | font-size: inherit; 154 | margin: 0; 155 | padding: 0; 156 | } 157 | .app-nav li ul a.active { 158 | border-bottom: 0; 159 | } 160 | .app-nav li:hover ul { 161 | display: block; 162 | } 163 | .github-corner { 164 | border-bottom: 0; 165 | position: fixed; 166 | right: 0; 167 | text-decoration: none; 168 | top: 0; 169 | z-index: 1; 170 | } 171 | .github-corner:hover .octo-arm { 172 | -webkit-animation: octocat-wave 0.56s ease-in-out; 173 | animation: octocat-wave 0.56s ease-in-out; 174 | } 175 | .github-corner svg { 176 | color: #fff; 177 | fill: var(--theme-color, #048fd6); 178 | height: 80px; 179 | width: 80px; 180 | } 181 | main { 182 | display: block; 183 | position: relative; 184 | width: 100vw; 185 | height: 100%; 186 | z-index: 0; 187 | } 188 | main.hidden { 189 | display: none; 190 | } 191 | .anchor { 192 | display: inline-block; 193 | text-decoration: none; 194 | transition: all 0.3s; 195 | } 196 | .anchor span { 197 | color: #34495e; 198 | } 199 | .anchor:hover { 200 | text-decoration: underline; 201 | } 202 | .sidebar { 203 | border-right: 1px solid rgba(0, 0, 0, 0.07); 204 | overflow-y: auto; 205 | padding: 40px 0 0; 206 | position: absolute; 207 | top: 0; 208 | bottom: 0; 209 | left: 0; 210 | transition: transform 0.25s ease-out; 211 | width: 300px; 212 | z-index: 20; 213 | } 214 | .sidebar > h1 { 215 | margin: 0 auto 1rem; 216 | font-size: 1.5rem; 217 | font-weight: 300; 218 | text-align: center; 219 | } 220 | .sidebar > h1 a { 221 | color: inherit; 222 | text-decoration: none; 223 | } 224 | .sidebar > h1 .app-nav { 225 | display: block; 226 | position: static; 227 | } 228 | .sidebar .sidebar-nav { 229 | line-height: 2em; 230 | padding-bottom: 40px; 231 | } 232 | .sidebar li.collapse .app-sub-sidebar { 233 | display: none; 234 | } 235 | .sidebar ul { 236 | margin: 0 0 0 15px; 237 | padding: 0; 238 | } 239 | .sidebar li > p { 240 | font-weight: 700; 241 | margin: 0; 242 | } 243 | .sidebar ul, 244 | .sidebar ul li { 245 | list-style: none; 246 | } 247 | .sidebar ul li a { 248 | border-bottom: none; 249 | display: block; 250 | } 251 | .sidebar ul li ul { 252 | padding-left: 20px; 253 | } 254 | .sidebar::-webkit-scrollbar { 255 | width: 4px; 256 | } 257 | .sidebar::-webkit-scrollbar-thumb { 258 | background: transparent; 259 | border-radius: 4px; 260 | } 261 | .sidebar:hover::-webkit-scrollbar-thumb { 262 | background: hsla(0, 0%, 53.3%, 0.4); 263 | } 264 | .sidebar:hover::-webkit-scrollbar-track { 265 | background: hsla(0, 0%, 53.3%, 0.1); 266 | } 267 | .sidebar-toggle { 268 | background-color: transparent; 269 | background-color: hsla(0, 0%, 100%, 0.8); 270 | border: 0; 271 | outline: none; 272 | padding: 10px; 273 | position: absolute; 274 | bottom: 0; 275 | left: 0; 276 | text-align: center; 277 | transition: opacity 0.3s; 278 | width: 284px; 279 | z-index: 30; 280 | cursor: pointer; 281 | } 282 | .sidebar-toggle:hover .sidebar-toggle-button { 283 | opacity: 0.4; 284 | } 285 | .sidebar-toggle span { 286 | background-color: var(--theme-color, #048fd6); 287 | display: block; 288 | margin-bottom: 4px; 289 | width: 16px; 290 | height: 2px; 291 | } 292 | body.sticky .sidebar, 293 | body.sticky .sidebar-toggle { 294 | position: fixed; 295 | } 296 | .content { 297 | padding-top: 60px; 298 | position: absolute; 299 | top: 0; 300 | right: 0; 301 | bottom: 0; 302 | left: 300px; 303 | transition: left 0.25s ease; 304 | } 305 | .markdown-section { 306 | margin: 0 auto; 307 | max-width: 80%; 308 | padding: 30px 15px 40px; 309 | position: relative; 310 | } 311 | .markdown-section > * { 312 | box-sizing: border-box; 313 | font-size: inherit; 314 | } 315 | .markdown-section > :first-child { 316 | margin-top: 0 !important; 317 | } 318 | .markdown-section hr { 319 | border: none; 320 | border-bottom: 1px solid #eee; 321 | margin: 2em 0; 322 | } 323 | .markdown-section iframe { 324 | border: 1px solid #eee; 325 | width: 1px; 326 | min-width: 100%; 327 | } 328 | .markdown-section table { 329 | border-collapse: collapse; 330 | border-spacing: 0; 331 | display: block; 332 | margin-bottom: 1rem; 333 | overflow: auto; 334 | width: 100%; 335 | } 336 | .markdown-section th { 337 | font-weight: 700; 338 | } 339 | .markdown-section td, 340 | .markdown-section th { 341 | border: 1px solid #ddd; 342 | padding: 6px 13px; 343 | } 344 | .markdown-section tr { 345 | border-top: 1px solid #ccc; 346 | } 347 | .markdown-section p.tip, 348 | .markdown-section tr:nth-child(2n) { 349 | background-color: #f8f8f8; 350 | } 351 | .markdown-section p.tip { 352 | border-bottom-right-radius: 2px; 353 | border-left: 4px solid #f66; 354 | border-top-right-radius: 2px; 355 | margin: 2em 0; 356 | padding: 12px 24px 12px 30px; 357 | position: relative; 358 | } 359 | .markdown-section p.tip:before { 360 | background-color: #f66; 361 | border-radius: 100%; 362 | color: #fff; 363 | content: '!'; 364 | font-family: Dosis, Source Sans Pro, Helvetica Neue, Arial, sans-serif; 365 | font-size: 14px; 366 | font-weight: 700; 367 | left: -12px; 368 | line-height: 20px; 369 | position: absolute; 370 | height: 20px; 371 | width: 20px; 372 | text-align: center; 373 | top: 14px; 374 | } 375 | .markdown-section p.tip code { 376 | background-color: #efefef; 377 | } 378 | .markdown-section p.tip em { 379 | color: #34495e; 380 | } 381 | .markdown-section p.warn { 382 | background: rgba(66, 185, 131, 0.1); 383 | border-radius: 2px; 384 | padding: 1rem; 385 | } 386 | .markdown-section ul.task-list > li { 387 | list-style-type: none; 388 | } 389 | body.close .sidebar { 390 | transform: translateX(-300px); 391 | } 392 | body.close .sidebar-toggle { 393 | width: auto; 394 | } 395 | body.close .content { 396 | left: 0; 397 | } 398 | @media print { 399 | .app-nav, 400 | .github-corner, 401 | .sidebar, 402 | .sidebar-toggle { 403 | display: none; 404 | } 405 | } 406 | @media screen and (max-width: 768px) { 407 | .github-corner, 408 | .sidebar, 409 | .sidebar-toggle { 410 | position: fixed; 411 | } 412 | .app-nav { 413 | margin-top: 16px; 414 | } 415 | .app-nav li ul { 416 | top: 30px; 417 | } 418 | main { 419 | height: auto; 420 | overflow-x: hidden; 421 | } 422 | .sidebar { 423 | left: -300px; 424 | transition: transform 0.25s ease-out; 425 | } 426 | .content { 427 | left: 0; 428 | max-width: 100vw; 429 | position: static; 430 | padding-top: 20px; 431 | transition: transform 0.25s ease; 432 | } 433 | .app-nav, 434 | .github-corner { 435 | transition: transform 0.25s ease-out; 436 | } 437 | .sidebar-toggle { 438 | background-color: transparent; 439 | width: auto; 440 | padding: 30px 30px 10px 10px; 441 | } 442 | body.close .sidebar { 443 | transform: translateX(300px); 444 | } 445 | body.close .sidebar-toggle { 446 | background-color: hsla(0, 0%, 100%, 0.8); 447 | transition: background-color 1s; 448 | width: 284px; 449 | padding: 10px; 450 | } 451 | body.close .content { 452 | transform: translateX(300px); 453 | } 454 | body.close .app-nav, 455 | body.close .github-corner { 456 | display: none; 457 | } 458 | .github-corner:hover .octo-arm { 459 | -webkit-animation: none; 460 | animation: none; 461 | } 462 | .github-corner .octo-arm { 463 | -webkit-animation: octocat-wave 0.56s ease-in-out; 464 | animation: octocat-wave 0.56s ease-in-out; 465 | } 466 | } 467 | @-webkit-keyframes octocat-wave { 468 | 0%, 469 | to { 470 | transform: rotate(0); 471 | } 472 | 20%, 473 | 60% { 474 | transform: rotate(-25deg); 475 | } 476 | 40%, 477 | 80% { 478 | transform: rotate(10deg); 479 | } 480 | } 481 | @keyframes octocat-wave { 482 | 0%, 483 | to { 484 | transform: rotate(0); 485 | } 486 | 20%, 487 | 60% { 488 | transform: rotate(-25deg); 489 | } 490 | 40%, 491 | 80% { 492 | transform: rotate(10deg); 493 | } 494 | } 495 | section.cover { 496 | align-items: center; 497 | background-position: 50%; 498 | background-repeat: no-repeat; 499 | background-size: cover; 500 | height: 100vh; 501 | width: 100vw; 502 | display: none; 503 | } 504 | section.cover.show { 505 | display: flex; 506 | } 507 | section.cover.has-mask .mask { 508 | background-color: #fff; 509 | opacity: 0.8; 510 | position: absolute; 511 | top: 0; 512 | height: 100%; 513 | width: 100%; 514 | } 515 | section.cover .cover-main { 516 | flex: 1; 517 | margin: -20px 16px 0; 518 | text-align: center; 519 | position: relative; 520 | } 521 | section.cover a { 522 | color: inherit; 523 | } 524 | section.cover a, 525 | section.cover a:hover { 526 | text-decoration: none; 527 | } 528 | section.cover p { 529 | line-height: 1.5rem; 530 | margin: 1em 0; 531 | } 532 | section.cover h1 { 533 | color: inherit; 534 | font-size: 2.5rem; 535 | font-weight: 300; 536 | margin: 0.625rem 0 2.5rem; 537 | position: relative; 538 | text-align: center; 539 | } 540 | section.cover h1 a { 541 | display: block; 542 | } 543 | section.cover h1 small { 544 | bottom: -0.4375rem; 545 | font-size: 1rem; 546 | position: absolute; 547 | } 548 | section.cover blockquote { 549 | font-size: 1.5rem; 550 | text-align: center; 551 | } 552 | section.cover ul { 553 | line-height: 1.8; 554 | list-style-type: none; 555 | margin: 1em auto; 556 | max-width: 500px; 557 | padding: 0; 558 | } 559 | section.cover .cover-main > p:last-child a { 560 | border-radius: 2rem; 561 | border: 1px solid var(--theme-color, #048fd6); 562 | box-sizing: border-box; 563 | color: var(--theme-color, #048fd6); 564 | display: inline-block; 565 | font-size: 1.05rem; 566 | letter-spacing: 0.1rem; 567 | margin: 0.5rem 1rem; 568 | padding: 0.75em 2rem; 569 | text-decoration: none; 570 | transition: all 0.15s ease; 571 | } 572 | section.cover .cover-main > p:last-child a:last-child { 573 | background-color: var(--theme-color, #048fd6); 574 | color: #fff; 575 | } 576 | section.cover .cover-main > p:last-child a:last-child:hover { 577 | color: inherit; 578 | opacity: 0.8; 579 | } 580 | section.cover .cover-main > p:last-child a:hover { 581 | color: inherit; 582 | } 583 | section.cover blockquote > p > a { 584 | border-bottom: 2px solid var(--theme-color, #048fd6); 585 | transition: color 0.3s; 586 | } 587 | section.cover blockquote > p > a:hover { 588 | color: var(--theme-color, #048fd6); 589 | } 590 | .sidebar, 591 | body { 592 | background-color: #fff; 593 | } 594 | .sidebar { 595 | color: #364149; 596 | } 597 | .sidebar li { 598 | margin: 6px 0; 599 | } 600 | .sidebar ul li a { 601 | color: #505d6b; 602 | font-size: 14px; 603 | font-weight: 400; 604 | overflow: hidden; 605 | text-decoration: none; 606 | text-overflow: ellipsis; 607 | white-space: nowrap; 608 | } 609 | .sidebar ul li a:hover { 610 | text-decoration: underline; 611 | } 612 | .sidebar ul li ul { 613 | padding: 0; 614 | } 615 | .sidebar ul li.active > a { 616 | border-right: 2px solid; 617 | color: var(--theme-color, #048fd6); 618 | font-weight: 600; 619 | } 620 | .app-sub-sidebar li:before { 621 | content: '-'; 622 | padding-right: 4px; 623 | float: left; 624 | } 625 | .markdown-section h1, 626 | .markdown-section h2, 627 | .markdown-section h3, 628 | .markdown-section h4, 629 | .markdown-section strong { 630 | color: #2c3e50; 631 | font-weight: 600; 632 | } 633 | .markdown-section a { 634 | color: var(--theme-color, #048fd6); 635 | font-weight: 600; 636 | } 637 | .markdown-section h1 { 638 | font-size: 2rem; 639 | margin: 0 0 1rem; 640 | } 641 | .markdown-section h2 { 642 | font-size: 1.75rem; 643 | margin: 45px 0 0.8rem; 644 | } 645 | .markdown-section h3 { 646 | font-size: 1.5rem; 647 | margin: 40px 0 0.6rem; 648 | } 649 | .markdown-section h4 { 650 | font-size: 1.25rem; 651 | } 652 | .markdown-section h5 { 653 | font-size: 1rem; 654 | } 655 | .markdown-section h6 { 656 | color: #777; 657 | font-size: 1rem; 658 | } 659 | .markdown-section figure, 660 | .markdown-section p { 661 | margin: 1.2em 0; 662 | } 663 | .markdown-section ol, 664 | .markdown-section p, 665 | .markdown-section ul { 666 | line-height: 1.6rem; 667 | word-spacing: 0.05rem; 668 | } 669 | .markdown-section ol, 670 | .markdown-section ul { 671 | padding-left: 1.5rem; 672 | } 673 | .markdown-section blockquote { 674 | border-left: 4px solid var(--theme-color, #048fd6); 675 | color: #858585; 676 | margin: 2em 0; 677 | padding-left: 20px; 678 | } 679 | .markdown-section blockquote p { 680 | font-weight: 600; 681 | margin-left: 0; 682 | } 683 | .markdown-section iframe { 684 | margin: 1em 0; 685 | } 686 | .markdown-section em { 687 | color: #7f8c8d; 688 | } 689 | .markdown-section code, 690 | .markdown-section output:after, 691 | .markdown-section pre { 692 | font-family: Roboto Mono, Monaco, courier, monospace; 693 | } 694 | .markdown-section code, 695 | .markdown-section pre { 696 | background-color: #f8f8f8; 697 | } 698 | .markdown-section output, 699 | .markdown-section pre { 700 | margin: 1.2em 0; 701 | position: relative; 702 | } 703 | .markdown-section output, 704 | .markdown-section pre > code { 705 | border-radius: 2px; 706 | display: block; 707 | } 708 | .markdown-section output:after, 709 | .markdown-section pre > code { 710 | -moz-osx-font-smoothing: initial; 711 | -webkit-font-smoothing: initial; 712 | } 713 | .markdown-section code { 714 | border-radius: 2px; 715 | color: #e96900; 716 | margin: 0 2px; 717 | padding: 3px 5px; 718 | white-space: pre-wrap; 719 | } 720 | .markdown-section > :not(h1):not(h2):not(h3):not(h4):not(h5):not(h6) code { 721 | font-size: 0.8rem; 722 | } 723 | .markdown-section pre { 724 | padding: 0 1.4rem; 725 | line-height: 1.5rem; 726 | overflow: auto; 727 | word-wrap: normal; 728 | } 729 | .markdown-section pre > code { 730 | color: #525252; 731 | font-size: 0.8rem; 732 | padding: 2.2em 5px; 733 | line-height: inherit; 734 | margin: 0 2px; 735 | max-width: inherit; 736 | overflow: inherit; 737 | white-space: inherit; 738 | } 739 | .markdown-section output { 740 | padding: 1.7rem 1.4rem; 741 | border: 1px dotted #ccc; 742 | } 743 | .markdown-section output > :first-child { 744 | margin-top: 0; 745 | } 746 | .markdown-section output > :last-child { 747 | margin-bottom: 0; 748 | } 749 | .markdown-section code:after, 750 | .markdown-section code:before, 751 | .markdown-section output:after, 752 | .markdown-section output:before { 753 | letter-spacing: 0.05rem; 754 | } 755 | .markdown-section output:after, 756 | .markdown-section pre:after { 757 | color: #ccc; 758 | font-size: 0.6rem; 759 | font-weight: 600; 760 | height: 15px; 761 | line-height: 15px; 762 | padding: 5px 10px 0; 763 | position: absolute; 764 | right: 0; 765 | text-align: right; 766 | top: 0; 767 | content: attr(data-lang); 768 | } 769 | .token.cdata, 770 | .token.comment, 771 | .token.doctype, 772 | .token.prolog { 773 | color: #8e908c; 774 | } 775 | .token.namespace { 776 | opacity: 0.7; 777 | } 778 | .token.boolean, 779 | .token.number { 780 | color: #c76b29; 781 | } 782 | .token.punctuation { 783 | color: #525252; 784 | } 785 | .token.property { 786 | color: #c08b30; 787 | } 788 | .token.tag { 789 | color: #2973b7; 790 | } 791 | .token.string { 792 | color: var(--theme-color, #048fd6); 793 | } 794 | .token.selector { 795 | color: #6679cc; 796 | } 797 | .token.attr-name { 798 | color: #2973b7; 799 | } 800 | .language-css .token.string, 801 | .style .token.string, 802 | .token.entity, 803 | .token.url { 804 | color: #22a2c9; 805 | } 806 | .token.attr-value, 807 | .token.control, 808 | .token.directive, 809 | .token.unit { 810 | color: var(--theme-color, #048fd6); 811 | } 812 | .token.function, 813 | .token.keyword { 814 | color: #e96900; 815 | } 816 | .token.atrule, 817 | .token.regex, 818 | .token.statement { 819 | color: #22a2c9; 820 | } 821 | .token.placeholder, 822 | .token.variable { 823 | color: #3d8fd1; 824 | } 825 | .token.deleted { 826 | text-decoration: line-through; 827 | } 828 | .token.inserted { 829 | border-bottom: 1px dotted #202746; 830 | text-decoration: none; 831 | } 832 | .token.italic { 833 | font-style: italic; 834 | } 835 | .token.bold, 836 | .token.important { 837 | font-weight: 700; 838 | } 839 | .token.important { 840 | color: #c94922; 841 | } 842 | .token.entity { 843 | cursor: help; 844 | } 845 | code .token { 846 | -moz-osx-font-smoothing: initial; 847 | -webkit-font-smoothing: initial; 848 | min-height: 1.5rem; 849 | position: relative; 850 | left: auto; 851 | } 852 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "aldenn", 3 | "license": "ISC", 4 | "name": "react-webpack-typescript-starter", 5 | "version": "0.0.5", 6 | "description": "The React-Typescript Boilerplate with Webpack config manual", 7 | "main": "index.js", 8 | "scripts": { 9 | "postinstall": "rm -rf .husky && husky install && npx husky add .husky/pre-commit \"npx lint-staged\"", 10 | "test": "jest --config src/jest.config.js --updateSnapshot", 11 | "dev": "webpack serve --config webpack/webpack.dev.js --hot", 12 | "build": "webpack --config webpack/webpack.prod.js", 13 | "start": "node server.js", 14 | "start-pm2": "pm2 start ./pm2/prod.json", 15 | "lint": "eslint .", 16 | "format": "prettier --write \"**/*.+(js|jsx|ts|json|css|md)\"" 17 | }, 18 | "lint-staged": { 19 | "*.+(js|jsx|ts|tsx)": "eslint --max-warnings 0 --fix", 20 | "*.+(js|jsx|ts|tsx|json|css|md)": "prettier --write" 21 | }, 22 | "dependencies": { 23 | "@reduxjs/toolkit": "^1.6.1", 24 | "axios": "^0.21.1", 25 | "connected-react-router": "^6.9.1", 26 | "i18next": "^20.4.0", 27 | "lodash": "^4.17.21", 28 | "pm2": "^5.1.0", 29 | "react": "^17.0.1", 30 | "react-dom": "^17.0.1", 31 | "react-helmet-async": "^1.0.9", 32 | "react-i18next": "^11.8.10", 33 | "react-redux": "^7.2.2", 34 | "react-router-dom": "^5.2.0", 35 | "react-toastify": "^7.0.3", 36 | "redux": "^4.0.5", 37 | "redux-thunk": "^2.3.0", 38 | "reselect": "^4.0.0" 39 | }, 40 | "devDependencies": { 41 | "@babel/plugin-transform-runtime": "^7.13.10", 42 | "@babel/preset-env": "^7.13.9", 43 | "@babel/preset-react": "^7.12.13", 44 | "@svgr/webpack": "^6.2.1", 45 | "@testing-library/jest-dom": "^5.11.10", 46 | "@testing-library/react": "^12.0.0", 47 | "@types/enzyme": "^3.10.9", 48 | "@types/enzyme-adapter-react-16": "^1.0.6", 49 | "@types/history": "^4.7.9", 50 | "@types/jest": "^27.0.1", 51 | "@types/lodash": "^4.14.172", 52 | "@types/react-dom": "^17.0.9", 53 | "@types/react-redux": "^7.1.18", 54 | "@types/react-router-dom": "^5.1.8", 55 | "@types/react-test-renderer": "^17.0.1", 56 | "@types/redux": "^3.6.0", 57 | "@types/redux-thunk": "^2.1.0", 58 | "@typescript-eslint/eslint-plugin": "^4.30.0", 59 | "@typescript-eslint/parser": "^4.29.3", 60 | "babel-jest": "^27.0.6", 61 | "babel-loader": "^8.2.2", 62 | "babel-plugin-lodash": "^3.3.4", 63 | "babel-preset-env": "^1.7.0", 64 | "babel-runtime": "^6.26.0", 65 | "copy-webpack-plugin": "^9.0.1", 66 | "cross-env": "^7.0.3", 67 | "css-loader": "^6.2.0", 68 | "css-minimizer-webpack-plugin": "^3.0.2", 69 | "dotenv": "^10.0.0", 70 | "dotenv-expand": "^5.1.0", 71 | "enzyme": "^3.11.0", 72 | "enzyme-adapter-react-16": "^1.15.6", 73 | "eslint": "^7.21.0", 74 | "eslint-loader": "^4.0.2", 75 | "eslint-plugin-react": "^7.22.0", 76 | "eslint-plugin-react-hooks": "^4.2.0", 77 | "file-loader": "^6.2.0", 78 | "html-loader": "^2.1.2", 79 | "html-webpack-plugin": "^5.3.1", 80 | "husky": "^7.0.0", 81 | "image-minimizer-webpack-plugin": "^2.2.0", 82 | "imagemin-gifsicle": "^7.0.0", 83 | "imagemin-jpegtran": "^7.0.0", 84 | "imagemin-optipng": "^8.0.0", 85 | "imagemin-svgo": "^9.0.0", 86 | "import-glob": "^1.5.0", 87 | "jest": "^27.0.6", 88 | "jest-watch-typeahead": "^0.6.2", 89 | "lint-staged": "^11.1.2", 90 | "lodash-webpack-plugin": "^0.11.6", 91 | "mini-css-extract-plugin": "1.3.9", 92 | "node-sass": "^6.0.1", 93 | "postcss": "^8.2.8", 94 | "postcss-flexbugs-fixes": "^5.0.2", 95 | "postcss-loader": "^6.1.1", 96 | "postcss-preset-env": "^6.7.0", 97 | "prettier": "^2.2.1", 98 | "react-dev-utils": "^11.0.4", 99 | "react-test-renderer": "^17.0.2", 100 | "resolve-url-loader": "^4.0.0", 101 | "sass-loader": "^12.1.0", 102 | "style-loader": "^3.2.1", 103 | "svg-url-loader": "^7.1.1", 104 | "terser-webpack-plugin": "^4.2.3", 105 | "ts-jest": "^27.0.5", 106 | "ts-loader": "^9.2.5", 107 | "typescript": "^4.4.2", 108 | "webpack": "^5.24.4", 109 | "webpack-bundle-analyzer": "^4.4.0", 110 | "webpack-cli": "^4.5.0", 111 | "webpack-dev-server": "^4.0.0", 112 | "webpack-manifest-plugin": "^4.0.2", 113 | "webpack-nano": "^1.1.1" 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pm2/prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "react-webpack-typescript-starter", 5 | "cwd": "./", 6 | "kill_timeout": 3000, 7 | "restart_delay": 3000, 8 | "script": "npm", 9 | "args": "run start" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaind97git/react-typescript-webpack/3b114fec926ef267433cc70669a564f612c29ad4/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Webpack Typescript Starter 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /public/static/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaind97git/react-typescript-webpack/3b114fec926ef267433cc70669a564f612c29ad4/public/static/images/banner.png -------------------------------------------------------------------------------- /public/static/images/icon/arrow-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/static/images/icon/en.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /public/static/images/icon/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 13 | 15 | 35 | 37 | 39 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /public/static/images/icon/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/static/images/icon/stack-overflow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/static/images/icon/vi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/static/images/icon/view-details.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 13 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /public/static/images/login-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaind97git/react-typescript-webpack/3b114fec926ef267433cc70669a564f612c29ad4/public/static/images/login-avatar.png -------------------------------------------------------------------------------- /public/static/images/webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaind97git/react-typescript-webpack/3b114fec926ef267433cc70669a564f612c29ad4/public/static/images/webpack.png -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const paths = require('./config/paths'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const PACKAGE = require(paths.appPackageJson); 5 | 6 | const app = express(); 7 | 8 | require('dotenv').config({ 9 | path: '.env.production', 10 | }); 11 | const environment = process.env.NODE_ENV || 'development'; 12 | const port = process.env.PORT || 3000; 13 | 14 | const DIST_DIR = path.join(__dirname, './build'); // NEW 15 | const HTML_FILE = path.join(DIST_DIR, 'index.html'); // NEW 16 | const mockResponse = { 17 | foo: 'bar', 18 | bar: 'foo', 19 | version: PACKAGE.version, 20 | }; 21 | 22 | app.use(express.static(DIST_DIR)); // NEW 23 | app.get('/ping', (req, res) => { 24 | res.send(mockResponse); 25 | }); 26 | app.get('*', (req, res) => { 27 | res.sendFile(HTML_FILE); // EDIT 28 | }); 29 | app.listen(port, function () { 30 | console.log('environment: ', environment); 31 | console.log(`> Ready on http://localhost:${port}`); 32 | }); 33 | -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRenderer } from 'react-test-renderer/shallow'; 3 | import App from './App'; 4 | 5 | const renderer = createRenderer(); 6 | 7 | it('renders without crashing', () => { 8 | renderer.render(); 9 | const renderedOutput = renderer.getRenderOutput(); 10 | expect(renderedOutput).toMatchSnapshot(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import { history } from '@/store'; 4 | import { ConnectedRouter } from 'connected-react-router'; 5 | import { ToastContainer } from 'react-toastify'; 6 | import { Helmet } from 'react-helmet-async'; 7 | 8 | import Layout from '@/layouts'; 9 | import PageLoading from '@/components/page-loading'; 10 | 11 | // multi language 12 | import '@/locales/i18n'; 13 | 14 | // load app SCSS styles 15 | import '@/styles/App.scss'; 16 | 17 | // load Toast styles 18 | import 'react-toastify/dist/ReactToastify.css'; 19 | 20 | const ReactApp: React.FC = () => { 21 | return ( 22 | 23 | 24 | 25 | 26 | 27 | 28 | }> 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default ReactApp; 40 | -------------------------------------------------------------------------------- /src/__snapshots__/App.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`renders without crashing 1`] = ` 4 | 5 | 12 | 16 | 17 | 39 | 44 | } 45 | > 46 | 47 | 48 | 49 | 50 | 66 | 67 | `; 68 | -------------------------------------------------------------------------------- /src/apis/auth.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import HttpRequest from '@/services/http-request'; 3 | 4 | export const getCurrentUser = async (): Promise => 5 | HttpRequest.get('/api/login/GetInfoToken'); 6 | -------------------------------------------------------------------------------- /src/apis/todo.ts: -------------------------------------------------------------------------------- 1 | import { AxiosResponse } from 'axios'; 2 | import HttpRequest from '@/services/http-request'; 3 | 4 | export const getTodoList = async (): Promise => 5 | HttpRequest.get('https://jsonplaceholder.typicode.com/todos'); 6 | 7 | export const getTodoDetails = async (id: string): Promise => 8 | HttpRequest.get(`https://jsonplaceholder.typicode.com/todos/${id}`); 9 | -------------------------------------------------------------------------------- /src/components/button/Button.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, cleanup } from '@testing-library/react'; 3 | import { shallow } from 'enzyme'; 4 | import Button from './index'; 5 | 6 | afterEach(cleanup); 7 | 8 | describe('This will test Button component', () => { 9 | test('test renders message', () => { 10 | const { getByText } = render(); 11 | 12 | expect(getByText('Click here')); 13 | }); 14 | 15 | it('test click event', () => { 16 | const mockCallBack = jest.fn(); 17 | 18 | const button = shallow(); 19 | button.find('button').simulate('click'); 20 | expect(mockCallBack.mock.calls.length).toEqual(1); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/button/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | interface IBtnProps { 4 | primary?: boolean; 5 | danger?: boolean; 6 | warning?: boolean; 7 | } 8 | const getBtnStringType = (props: IBtnProps): string => { 9 | const { primary, danger, warning } = props; 10 | 11 | switch (true) { 12 | case primary: 13 | return 'primary'; 14 | case danger: 15 | return 'danger'; 16 | case warning: 17 | return 'warning'; 18 | default: 19 | return 'default'; 20 | } 21 | }; 22 | 23 | interface IProps extends IBtnProps { 24 | children: ReactElement | string; 25 | } 26 | 27 | const Button: React.FC> = ({ 28 | primary, 29 | danger, 30 | warning, 31 | children, 32 | ...others 33 | }) => { 34 | const btnClass = [getBtnStringType({ primary, danger, warning })].join(' '); 35 | 36 | return ( 37 | 40 | ); 41 | }; 42 | 43 | export default Button; 44 | -------------------------------------------------------------------------------- /src/components/button/style.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | display: inline-block; 3 | font-weight: 400; 4 | text-align: center; 5 | white-space: nowrap; 6 | vertical-align: middle; 7 | -webkit-user-select: none; 8 | -moz-user-select: none; 9 | -ms-user-select: none; 10 | user-select: none; 11 | border: 1px solid transparent; 12 | padding: 0.375rem 0.75rem; 13 | font-size: 1rem; 14 | line-height: 1.5; 15 | border-radius: 0.25rem; 16 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 17 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 18 | outline: none; 19 | cursor: pointer; 20 | border-radius: 8px; 21 | overflow: hidden; 22 | transform: translateX(0); 23 | } 24 | 25 | .btn-primary { 26 | background: $primary-color; 27 | color: white; 28 | // box-shadow: 0 6px 30px -10px rgba($primary-color, 1); 29 | } 30 | 31 | .btn-danger { 32 | color: #fff; 33 | background: $error-color; 34 | // box-shadow: 0 6px 30px -10px rgba($error-color, 1); 35 | } 36 | 37 | .btn-warning { 38 | color: #fff; 39 | background: $warning-color; 40 | // box-shadow: 0 6px 30px -10px rgba($warning-color, 1); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/not-found/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | const NotFound: React.FC = () => { 4 | return ( 5 |
6 |

404

7 |

Oops! Something is wrong.

8 | 9 | Go back in initial page, is better. 10 | 11 |
12 | ); 13 | }; 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /src/components/not-found/style.scss: -------------------------------------------------------------------------------- 1 | /*====================== 2 | 404 page 3 | =======================*/ 4 | 5 | .page_404 { 6 | background-color: #007aff; 7 | height: 100vh; 8 | .button { 9 | font-weight: 300; 10 | color: #fff; 11 | font-size: 1.2em; 12 | text-decoration: none; 13 | border: 1px solid #efefef; 14 | padding: 0.5em; 15 | border-radius: 3px; 16 | float: left; 17 | margin: 6em 0 0 -155px; 18 | left: 50%; 19 | position: relative; 20 | transition: all 0.3s linear; 21 | } 22 | 23 | .button:hover { 24 | background-color: #007aff; 25 | color: #fff; 26 | } 27 | 28 | p { 29 | font-size: 2em; 30 | text-align: center; 31 | font-weight: 100; 32 | color: white; 33 | } 34 | 35 | h1 { 36 | color: white; 37 | margin: 0; 38 | text-align: center; 39 | font-size: 15em; 40 | font-weight: 100; 41 | text-shadow: #0062cc 1px 1px, #0062cc 2px 2px, #0062cc 3px 3px, 42 | #0062cd 4px 4px, #0062cd 5px 5px, #0062cd 6px 6px, #0062cd 7px 7px, 43 | #0062ce 8px 8px, #0063ce 9px 9px, #0063ce 10px 10px, #0063ce 11px 11px, 44 | #0063cf 12px 12px, #0063cf 13px 13px, #0063cf 14px 14px, #0063cf 15px 15px, 45 | #0063d0 16px 16px, #0064d0 17px 17px, #0064d0 18px 18px, #0064d0 19px 19px, 46 | #0064d1 20px 20px, #0064d1 21px 21px, #0064d1 22px 22px, #0064d1 23px 23px, 47 | #0064d2 24px 24px, #0065d2 25px 25px, #0065d2 26px 26px, #0065d2 27px 27px, 48 | #0065d3 28px 28px, #0065d3 29px 29px, #0065d3 30px 30px, #0065d3 31px 31px, 49 | #0065d4 32px 32px, #0065d4 33px 33px, #0066d4 34px 34px, #0066d4 35px 35px, 50 | #0066d5 36px 36px, #0066d5 37px 37px, #0066d5 38px 38px, #0066d5 39px 39px, 51 | #0066d6 40px 40px, #0066d6 41px 41px, #0067d6 42px 42px, #0067d6 43px 43px, 52 | #0067d7 44px 44px, #0067d7 45px 45px, #0067d7 46px 46px, #0067d7 47px 47px, 53 | #0067d8 48px 48px, #0067d8 49px 49px, #0068d8 50px 50px, #0068d9 51px 51px, 54 | #0068d9 52px 52px, #0068d9 53px 53px, #0068d9 54px 54px, #0068da 55px 55px, 55 | #0068da 56px 56px, #0068da 57px 57px, #0068da 58px 58px, #0069db 59px 59px, 56 | #0069db 60px 60px, #0069db 61px 61px, #0069db 62px 62px, #0069dc 63px 63px, 57 | #0069dc 64px 64px, #0069dc 65px 65px, #0069dc 66px 66px, #006add 67px 67px, 58 | #006add 68px 68px, #006add 69px 69px, #006add 70px 70px, #006ade 71px 71px, 59 | #006ade 72px 72px, #006ade 73px 73px, #006ade 74px 74px, #006bdf 75px 75px, 60 | #006bdf 76px 76px, #006bdf 77px 77px, #006bdf 78px 78px, #006be0 79px 79px, 61 | #006be0 80px 80px, #006be0 81px 81px, #006be0 82px 82px, #006be1 83px 83px, 62 | #006ce1 84px 84px, #006ce1 85px 85px, #006ce1 86px 86px, #006ce2 87px 87px, 63 | #006ce2 88px 88px, #006ce2 89px 89px, #006ce2 90px 90px, #006ce3 91px 91px, 64 | #006de3 92px 92px, #006de3 93px 93px, #006de3 94px 94px, #006de4 95px 95px, 65 | #006de4 96px 96px, #006de4 97px 97px, #006de4 98px 98px, #006de5 99px 99px, 66 | #006ee5 100px 100px, #006ee5 101px 101px, #006ee6 102px 102px, 67 | #006ee6 103px 103px, #006ee6 104px 104px, #006ee6 105px 105px, 68 | #006ee7 106px 106px, #006ee7 107px 107px, #006ee7 108px 108px, 69 | #006fe7 109px 109px, #006fe8 110px 110px, #006fe8 111px 111px, 70 | #006fe8 112px 112px, #006fe8 113px 113px, #006fe9 114px 114px, 71 | #006fe9 115px 115px, #006fe9 116px 116px, #0070e9 117px 117px, 72 | #0070ea 118px 118px, #0070ea 119px 119px, #0070ea 120px 120px, 73 | #0070ea 121px 121px, #0070eb 122px 122px, #0070eb 123px 123px, 74 | #0070eb 124px 124px, #0071eb 125px 125px, #0071ec 126px 126px, 75 | #0071ec 127px 127px, #0071ec 128px 128px, #0071ec 129px 129px, 76 | #0071ed 130px 130px, #0071ed 131px 131px, #0071ed 132px 132px, 77 | #0071ed 133px 133px, #0072ee 134px 134px, #0072ee 135px 135px, 78 | #0072ee 136px 136px, #0072ee 137px 137px, #0072ef 138px 138px, 79 | #0072ef 139px 139px, #0072ef 140px 140px, #0072ef 141px 141px, 80 | #0073f0 142px 142px, #0073f0 143px 143px, #0073f0 144px 144px, 81 | #0073f0 145px 145px, #0073f1 146px 146px, #0073f1 147px 147px, 82 | #0073f1 148px 148px, #0073f1 149px 149px, #0074f2 150px 150px, 83 | #0074f2 151px 151px, #0074f2 152px 152px, #0074f3 153px 153px, 84 | #0074f3 154px 154px, #0074f3 155px 155px, #0074f3 156px 156px, 85 | #0074f4 157px 157px, #0074f4 158px 158px, #0075f4 159px 159px, 86 | #0075f4 160px 160px, #0075f5 161px 161px, #0075f5 162px 162px, 87 | #0075f5 163px 163px, #0075f5 164px 164px, #0075f6 165px 165px, 88 | #0075f6 166px 166px, #0076f6 167px 167px, #0076f6 168px 168px, 89 | #0076f7 169px 169px, #0076f7 170px 170px, #0076f7 171px 171px, 90 | #0076f7 172px 172px, #0076f8 173px 173px, #0076f8 174px 174px, 91 | #0077f8 175px 175px, #0077f8 176px 176px, #0077f9 177px 177px, 92 | #0077f9 178px 178px, #0077f9 179px 179px, #0077f9 180px 180px, 93 | #0077fa 181px 181px, #0077fa 182px 182px, #0077fa 183px 183px, 94 | #0078fa 184px 184px, #0078fb 185px 185px, #0078fb 186px 186px, 95 | #0078fb 187px 187px, #0078fb 188px 188px, #0078fc 189px 189px, 96 | #0078fc 190px 190px, #0078fc 191px 191px, #0079fc 192px 192px, 97 | #0079fd 193px 193px, #0079fd 194px 194px, #0079fd 195px 195px, 98 | #0079fd 196px 196px, #0079fe 197px 197px, #0079fe 198px 198px, 99 | #0079fe 199px 199px, #007aff 200px 200px; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/components/page-loading/index.tsx: -------------------------------------------------------------------------------- 1 | import { selectLoading } from '@/store/slices/appSlice'; 2 | import React from 'react'; 3 | import { useSelector } from 'react-redux'; 4 | import Spinner from '../spinner'; 5 | 6 | interface IProps { 7 | show?: boolean; 8 | } 9 | 10 | const PageLoading: React.FC = ({ show }) => { 11 | const { loading } = useSelector(selectLoading); 12 | const showLoading = typeof show === 'boolean' ? show : loading; 13 | 14 | return ( 15 |
19 |
20 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default PageLoading; 27 | -------------------------------------------------------------------------------- /src/components/page-loading/style.scss: -------------------------------------------------------------------------------- 1 | .page-loading { 2 | height: 100vh; 3 | width: 100%; 4 | position: fixed; 5 | top: 0px; 6 | left: 0px; 7 | top: 0px; 8 | bottom: 0px; 9 | z-index: 9999; 10 | background-color: rgba(255, 255, 255, 0.2); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/select/index.tsx: -------------------------------------------------------------------------------- 1 | import useOutsideClick from '@/hooks/useOutsideClick'; 2 | import React, { useRef, useState } from 'react'; 3 | import ArrowDown from '@/static/images/icon/arrow-down.svg'; 4 | 5 | interface IOption { 6 | value: any; 7 | label: any; 8 | } 9 | 10 | interface IProps { 11 | options: Array; 12 | width?: number; 13 | onChange?: (option: IOption) => void; 14 | className?: string; 15 | defaultValue?: any; 16 | } 17 | 18 | const Select: React.FC = ({ 19 | options, 20 | width = 240, 21 | onChange, 22 | className, 23 | defaultValue, 24 | }) => { 25 | const [show, setShow] = useState(false); 26 | const [option, setOption] = useState( 27 | options.find(opt => opt.value === defaultValue) || options[0], 28 | ); 29 | const selectRef = useRef(null); 30 | useOutsideClick(selectRef, () => { 31 | show === true && setShow(false); 32 | }); 33 | 34 | function handleSelectDropdown(option: IOption) { 35 | const opt = options.find(opt => opt.value === option.value); 36 | 37 | typeof onChange === 'function' && onChange(opt); 38 | setOption(opt); 39 | setShow(false); 40 | } 41 | return ( 42 |
47 |
setShow(true)} className="dropdown-select"> 48 | {option.label} 49 | 50 |
51 |
    52 | {options.map(opt => ( 53 |
  • handleSelectDropdown(opt)} 56 | className="dropdown-item" 57 | > 58 | {opt.label} 59 |
  • 60 | ))} 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Select; 67 | -------------------------------------------------------------------------------- /src/components/select/style.scss: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | color: $primary-color; 3 | width: 100%; 4 | position: relative; 5 | border-radius: 8px; 6 | .dropdown-caret { 7 | width: 12px; 8 | fill: $primary-color; 9 | 10 | &.up { 11 | transform: rotate(180deg); 12 | } 13 | } 14 | .dropdown-select { 15 | background-color: white; 16 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1); 17 | padding: 1rem; 18 | border-radius: inherit; 19 | display: flex; 20 | align-items: center; 21 | justify-content: space-between; 22 | cursor: pointer; 23 | } 24 | .dropdown-select * { 25 | pointer-events: none; 26 | } 27 | .dropdown-list { 28 | position: absolute; 29 | top: 100%; 30 | left: 0; 31 | right: 0; 32 | margin-top: 0.4rem; 33 | background-color: white; 34 | box-shadow: 0 0 15px 0 rgba(0, 0, 0, 0.1); 35 | padding: 1rem; 36 | border-radius: 8px; 37 | display: none; 38 | &::before { 39 | content: ''; 40 | height: 1rem; 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | right: 0; 45 | background-color: transparent; 46 | transform: translateY(-100%); 47 | } 48 | &.show { 49 | display: block; 50 | } 51 | 52 | .dropdown-item { 53 | padding: 1rem; 54 | color: #47536b; 55 | transition: all 0.25s ease; 56 | border-radius: 8px; 57 | cursor: pointer; 58 | &:hover { 59 | color: $primary-color; 60 | background-color: #f1fbff; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/components/spinner/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Spinner: React.FC = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 | ); 11 | }; 12 | 13 | export default Spinner; 14 | -------------------------------------------------------------------------------- /src/components/spinner/style.scss: -------------------------------------------------------------------------------- 1 | .loading-element { 2 | position: absolute; 3 | top: calc(50% - 32px); 4 | left: calc(50% - 32px); 5 | width: 64px; 6 | height: 64px; 7 | border-radius: 50%; 8 | perspective: 800px; 9 | } 10 | .loading-element .inner { 11 | position: absolute; 12 | box-sizing: border-box; 13 | width: 100%; 14 | height: 100%; 15 | border-radius: 50%; 16 | } 17 | .loading-element .inner.one { 18 | left: 0%; 19 | top: 0%; 20 | animation: rotate-one 1s linear infinite; 21 | border-bottom: 3px solid $primary-color; 22 | } 23 | .loading-element .inner.two { 24 | right: 0%; 25 | top: 0%; 26 | animation: rotate-two 1s linear infinite; 27 | border-right: 3px solid $primary-color; 28 | } 29 | .loading-element .inner.three { 30 | right: 0%; 31 | bottom: 0%; 32 | animation: rotate-three 1s linear infinite; 33 | border-top: 3px solid $primary-color; 34 | } 35 | .loading-element.white .inner.one { 36 | border-bottom: 3px solid #efeffa; 37 | } 38 | .loading-element.white .inner.two { 39 | border-right: 3px solid #efeffa; 40 | } 41 | .loading-element.white .inner.three { 42 | border-top: 3px solid #efeffa; 43 | } 44 | @keyframes rotate-one { 45 | 0% { 46 | transform: rotateX(35deg) rotateY(-45deg) rotateZ(0deg); 47 | } 48 | 100% { 49 | transform: rotateX(35deg) rotateY(-45deg) rotateZ(360deg); 50 | } 51 | } 52 | @keyframes rotate-two { 53 | 0% { 54 | transform: rotateX(50deg) rotateY(10deg) rotateZ(0deg); 55 | } 56 | 100% { 57 | transform: rotateX(50deg) rotateY(10deg) rotateZ(360deg); 58 | } 59 | } 60 | @keyframes rotate-three { 61 | 0% { 62 | transform: rotateX(35deg) rotateY(55deg) rotateZ(0deg); 63 | } 64 | 100% { 65 | transform: rotateX(35deg) rotateY(55deg) rotateZ(360deg); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/constants/env.ts: -------------------------------------------------------------------------------- 1 | const { NODE_ENV, API_SERVER_URL, PORT } = process.env; 2 | 3 | export default { 4 | NodeEnv: NODE_ENV, 5 | ApiServerUrl: API_SERVER_URL, 6 | Port: PORT, 7 | }; 8 | -------------------------------------------------------------------------------- /src/features/core/components/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Login: React.FC = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 | 14 |

15 | Forgot password? 16 |

17 |
18 |
19 |
20 | 21 |

22 | {"Don't have an account?"} Sign Up 23 |

24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Login; 30 | -------------------------------------------------------------------------------- /src/features/core/components/login/style.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | background-color: #fff; 3 | width: 330px; 4 | height: 570px; 5 | margin: 2em auto; 6 | border-radius: 5px; 7 | padding: 1em; 8 | position: relative; 9 | overflow: hidden; 10 | box-shadow: 0 6px 31px -2px rgba(0, 0, 0, 0.3); 11 | 12 | .bg { 13 | width: 400px; 14 | height: 550px; 15 | background: $primary-color; 16 | position: absolute; 17 | top: -5em; 18 | left: 0; 19 | right: 0; 20 | margin: auto; 21 | background-position: center; 22 | background-size: cover; 23 | background-repeat: no-repeat; 24 | clip-path: ellipse(69% 46% at 48% 46%); 25 | } 26 | form { 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | right: 0; 31 | text-align: center; 32 | padding: 2em; 33 | 34 | header { 35 | width: 220px; 36 | height: 220px; 37 | margin: 1em auto; 38 | background-color: transparent; 39 | img { 40 | max-width: 100%; 41 | } 42 | } 43 | 44 | .inputs { 45 | margin-top: -4em; 46 | position: relative; 47 | input { 48 | width: 100%; 49 | padding: 13px 15px; 50 | margin: 0.7em auto; 51 | border-radius: 100px; 52 | border: none; 53 | background: rgba(255, 255, 255, 0.3); 54 | outline: none; 55 | color: #fff; 56 | &::placeholder { 57 | color: #fff; 58 | font-size: 13px; 59 | } 60 | } 61 | 62 | .light { 63 | text-align: right; 64 | color: #fff; 65 | a { 66 | color: #fff; 67 | } 68 | } 69 | } 70 | } 71 | footer { 72 | position: absolute; 73 | bottom: 0; 74 | left: 0; 75 | right: 0; 76 | padding: 2em; 77 | text-align: center; 78 | a { 79 | text-decoration: none; 80 | color: $primary-color; 81 | } 82 | p { 83 | font-size: 13px; 84 | color: $text-color; 85 | line-height: 2; 86 | } 87 | button { 88 | cursor: pointer; 89 | width: 100%; 90 | padding: 13px 15px; 91 | border-radius: 100px; 92 | border: none; 93 | background: $primary-color; 94 | outline: none; 95 | color: #fff; 96 | } 97 | } 98 | } 99 | 100 | @include s-mobile { 101 | .login { 102 | height: 90vh; 103 | border-radius: 0; 104 | .bg { 105 | top: -10em; 106 | width: 100%; 107 | height: 90vh; 108 | } 109 | form { 110 | header { 111 | width: 90%; 112 | height: 100%; 113 | } 114 | .inputs { 115 | margin: 0; 116 | } 117 | } 118 | } 119 | 120 | input { 121 | padding: 18px 15px; 122 | } 123 | button { 124 | padding: 18px 15px; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/features/core/route.ts: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | 3 | const Login = lazy(() => import('./components/login/index')); 4 | 5 | export default [ 6 | { 7 | name: 'login', 8 | path: '/login', 9 | exact: true, 10 | layout: { 11 | header: false, 12 | footer: false, 13 | }, 14 | component: Login, 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /src/features/home/components/Home.tsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thaind97git/react-typescript-webpack/3b114fec926ef267433cc70669a564f612c29ad4/src/features/home/components/Home.tsx -------------------------------------------------------------------------------- /src/features/home/components/tag/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ITag } from '../../types'; 3 | 4 | const HomeTag: React.FC = ({ color, label }) => { 5 | return ( 6 | 10 | {label} 11 | 12 | ); 13 | }; 14 | 15 | export default HomeTag; 16 | -------------------------------------------------------------------------------- /src/features/home/components/tag/style.scss: -------------------------------------------------------------------------------- 1 | .home-tag { 2 | &.label { 3 | display: inline-flex; 4 | flex-wrap: wrap; 5 | } 6 | &.label-item { 7 | color: white; 8 | padding: 8px 0.5rem; 9 | border-radius: 6px; 10 | margin-right: 0.4rem; 11 | margin-bottom: 1rem; 12 | text-transform: uppercase; 13 | font-size: 12px; 14 | font-weight: bold; 15 | background-color: #333; 16 | display: inline-block; 17 | 18 | &.info { 19 | background-color: #00aefd; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/features/home/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { ITag } from './types'; 4 | import HomeTag from './components/tag'; 5 | import Select from '@/components/select'; 6 | import i18n from '@/locales/i18n'; 7 | 8 | import Banner from '@/static/images/banner.png'; 9 | import ViIcon from '@/static/images/icon/vi.svg'; 10 | import EnIcon from '@/static/images/icon/en.svg'; 11 | 12 | const keywords: Array = [ 13 | { label: 'React.js' }, 14 | { color: '#84c6e8', label: 'Webpack 5' }, 15 | { color: '#764abc', label: 'Redux' }, 16 | { color: '#f5da55', label: 'Babel' }, 17 | { color: '#15c213', label: 'jest' }, 18 | { color: '#e94949', label: 'react-router' }, 19 | { color: '#bf4080', label: 'sass' }, 20 | { color: '#764abc', label: 'redux-thunk' }, 21 | { color: '#2b037a', label: 'pm2' }, 22 | ]; 23 | 24 | const languageOptions = [ 25 | { 26 | label: ( 27 |
28 |    Vietnamese 29 |
30 | ), 31 | value: 'vi', 32 | }, 33 | { 34 | label: ( 35 |
36 | 37 |    English 38 |
39 | ), 40 | value: 'en', 41 | }, 42 | ]; 43 | 44 | const Home: React.FC = () => { 45 | const { t } = useTranslation(); 46 | return ( 47 |
48 |
49 | 50 |