├── src
├── assets
│ ├── styles
│ │ ├── tailwind.css
│ │ ├── main.css
│ │ ├── vars
│ │ │ └── _color.css
│ │ └── layout
│ │ │ ├── _init.css
│ │ │ └── _reset.css
│ └── static
│ │ └── images
│ │ ├── readme
│ │ ├── step_03.jpg
│ │ ├── step_04.jpg
│ │ ├── step_05.jpg
│ │ ├── step_07.jpg
│ │ ├── top-banner.jpg
│ │ ├── deploy-vercel.jpg
│ │ ├── deploy-onrender.jpg
│ │ ├── tiki-lighthouse.jpg
│ │ └── bandwidth-variable.jpg
│ │ ├── development
│ │ ├── webp
│ │ │ ├── ninja.webp
│ │ │ ├── minion.webp
│ │ │ └── kungfu-panda.webp
│ │ ├── png
│ │ │ └── kunfu_panda
│ │ │ │ ├── 01.png
│ │ │ │ ├── 02.png
│ │ │ │ ├── 03.png
│ │ │ │ ├── 04.png
│ │ │ │ └── 05.png
│ │ ├── gif
│ │ │ ├── kung-fu-panda-hey.gif
│ │ │ ├── kung-fu-panda-bored.gif
│ │ │ └── kung-fu-panda-pain.gif
│ │ └── svg
│ │ │ ├── diamond.svg
│ │ │ ├── bitcoin.svg
│ │ │ ├── chili.svg
│ │ │ ├── sycee-gold.svg
│ │ │ ├── cannabis.svg
│ │ │ ├── onion-green.svg
│ │ │ ├── money.svg
│ │ │ ├── bug.svg
│ │ │ ├── onion-purple.svg
│ │ │ └── medicines.svg
│ │ ├── icons
│ │ └── image-loading-icon.png
│ │ └── logo.svg
├── pages
│ ├── NotFoundPage.tsx
│ ├── HomePage.tsx
│ ├── CommentPage.tsx
│ ├── LoginPage.tsx
│ └── ContentPage.tsx
├── components
│ ├── ErrorPageComponent.tsx
│ ├── comment-page
│ │ ├── CommentSection.tsx
│ │ ├── CommentLoader.tsx
│ │ └── CommentRow.tsx
│ ├── Link.tsx
│ ├── ImageItem.tsx
│ ├── content-page
│ │ └── ModuleContentSection.tsx
│ ├── LoadingPageComponent.tsx
│ └── home-page
│ │ └── ModuleSection.tsx
├── app
│ ├── router
│ │ ├── context
│ │ │ ├── ValidationContext.ts
│ │ │ ├── LoadingInfoContext.tsx
│ │ │ └── InfoContext.ts
│ │ ├── hooks
│ │ │ └── useCertificateCustomizationInfo.ts
│ │ ├── utils
│ │ │ ├── RouterInit.tsx
│ │ │ ├── LazyComponentHandler.tsx
│ │ │ ├── RouterValidation.tsx
│ │ │ ├── RouterDeliver.tsx
│ │ │ └── RouterProtection.tsx
│ │ ├── types.ts
│ │ └── index.tsx
│ └── store
│ │ └── UserInfoContext.tsx
├── index.tsx
├── utils
│ ├── ErrorBoundary.tsx
│ ├── CookieHelper.ts
│ ├── LoadingBoundary.tsx
│ └── Suspender.ts
├── static.d.ts
├── hooks
│ └── useStringHelper.ts
└── Layout.tsx
├── .prettierignore
├── env
├── env.api.mjs
├── env.assets.mjs
├── env.project.mjs
├── env.general.mjs
├── env-register.mjs
├── env.style.mjs
└── env.router.mjs
├── config
├── utils
│ ├── PortHandler
│ │ ├── .env
│ │ └── index.js
│ ├── ObjectToEnvConverter.js
│ ├── StyleLoader.js
│ ├── SocketHandler.js
│ ├── NetworkGenerator.js
│ └── WebpackCustomizeDefinePlugin.js
├── postcss.config.js
├── prettier.config.js
├── commit-packages
│ └── package.json
├── webpack.serve.config.js
├── lint-and-format-packages
│ └── package.json
├── types
│ ├── ImportMeta.d.ts
│ └── dts-generator.mjs
├── build-tool-packages
│ └── package.json
├── eslint.config.js
├── cz.config.js
├── .git-cz.json
├── .eslintrc-auto-import.json
├── env
│ └── env.mjs
├── auto-imports.d.ts
├── webpack.production.config.js
├── webpack.development.config.js
└── templates
│ └── index.development.image-loading.html
├── .husky
├── pre-commit
├── commit-msg
└── prepare-commit-msg
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── .editorconfig
├── index.production.html
├── package.json
├── tsconfig.json
├── tailwind.config.js
├── webpack.config.js
└── README.md
/src/assets/styles/tailwind.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/utilities';
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore all HTML files:
2 | *.html
3 | config/**/*.d.ts
4 |
--------------------------------------------------------------------------------
/env/env.api.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: 'api',
3 | data: {},
4 | }
5 |
--------------------------------------------------------------------------------
/config/utils/PortHandler/.env:
--------------------------------------------------------------------------------
1 | PUPPETEER_SSR_PORT=8081
2 | SOCKET_IO_PORT=3030
3 |
--------------------------------------------------------------------------------
/env/env.assets.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: 'assets',
3 | data: {},
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/styles/main.css:
--------------------------------------------------------------------------------
1 | @import 'vars/_color';
2 | @import 'layout/_reset';
3 |
--------------------------------------------------------------------------------
/env/env.project.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: 'project',
3 | data: {},
4 | }
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run pre-commit
5 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/env/env.general.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: 'general',
3 | data: {
4 | greeting: 'React 18',
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/src/pages/NotFoundPage.tsx:
--------------------------------------------------------------------------------
1 | export default function NotFoundPage() {
2 | return
Page Not Found!!!
3 | }
4 |
--------------------------------------------------------------------------------
/src/assets/styles/vars/_color.css:
--------------------------------------------------------------------------------
1 | $dark-color: #020100;
2 | $yellow-color: #f1d302;
3 | $blue-color: #235789;
4 | $white-color: #fdfffc;
5 |
--------------------------------------------------------------------------------
/src/components/ErrorPageComponent.tsx:
--------------------------------------------------------------------------------
1 | export default function ErrorLoadingPageComponent() {
2 | return custom được rồi nè
3 | }
4 |
--------------------------------------------------------------------------------
/.husky/prepare-commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | exec < /dev/tty && node_modules/.bin/cz --hook || true
5 |
--------------------------------------------------------------------------------
/env/env-register.mjs:
--------------------------------------------------------------------------------
1 | import ENV_ROUTER from './env.router.mjs'
2 | import ENV_STYLE from './env.style.mjs'
3 |
4 | export default [ENV_ROUTER, ENV_STYLE]
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | package-lock.json
3 | yarn.lock
4 | node_modules
5 | dist
6 | config/env/.env
7 | config/env/env.json
8 | config/.DS_Store
9 | *.DS_Store
10 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "TabNine.tabnine-vscode",
4 | "ionutvmi.path-autocomplete",
5 | "rohit-gohri.format-code-action"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/config/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | 'postcss-preset-env',
4 | 'autoprefixer',
5 | require('tailwindcss')('./tailwind.config.js'),
6 | ],
7 | }
8 |
--------------------------------------------------------------------------------
/src/assets/static/images/readme/step_03.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/step_03.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/readme/step_04.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/step_04.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/readme/step_05.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/step_05.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/readme/step_07.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/step_07.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/readme/top-banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/top-banner.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/readme/deploy-vercel.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/deploy-vercel.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/development/webp/ninja.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/webp/ninja.webp
--------------------------------------------------------------------------------
/src/assets/static/images/readme/deploy-onrender.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/deploy-onrender.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/readme/tiki-lighthouse.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/tiki-lighthouse.jpg
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [**]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | trim_trailing_whitespace = true
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/webp/minion.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/webp/minion.webp
--------------------------------------------------------------------------------
/src/assets/static/images/icons/image-loading-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/icons/image-loading-icon.png
--------------------------------------------------------------------------------
/src/assets/static/images/readme/bandwidth-variable.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/readme/bandwidth-variable.jpg
--------------------------------------------------------------------------------
/src/assets/static/images/development/png/kunfu_panda/01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/png/kunfu_panda/01.png
--------------------------------------------------------------------------------
/src/assets/static/images/development/png/kunfu_panda/02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/png/kunfu_panda/02.png
--------------------------------------------------------------------------------
/src/assets/static/images/development/png/kunfu_panda/03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/png/kunfu_panda/03.png
--------------------------------------------------------------------------------
/src/assets/static/images/development/png/kunfu_panda/04.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/png/kunfu_panda/04.png
--------------------------------------------------------------------------------
/src/assets/static/images/development/png/kunfu_panda/05.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/png/kunfu_panda/05.png
--------------------------------------------------------------------------------
/src/assets/static/images/development/webp/kungfu-panda.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/webp/kungfu-panda.webp
--------------------------------------------------------------------------------
/env/env.style.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: 'style',
3 | data: {
4 | color: {
5 | dark: '#020100',
6 | yellow: '#f1d302',
7 | blue: '#235789',
8 | white: '#fdfffc',
9 | },
10 | },
11 | }
12 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/gif/kung-fu-panda-hey.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/gif/kung-fu-panda-hey.gif
--------------------------------------------------------------------------------
/src/assets/static/images/development/gif/kung-fu-panda-bored.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/gif/kung-fu-panda-bored.gif
--------------------------------------------------------------------------------
/src/assets/static/images/development/gif/kung-fu-panda-pain.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anhchangvt1994/webpack-project--template-react-ts__react-router/HEAD/src/assets/static/images/development/gif/kung-fu-panda-pain.gif
--------------------------------------------------------------------------------
/config/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSameLine: false,
3 | semi: false,
4 | singleQuote: true,
5 | useTabs: true,
6 | printWidth: 80,
7 | trailingComma: 'es5',
8 | vueIndentScriptAndStyle: true,
9 | }
10 |
--------------------------------------------------------------------------------
/src/assets/styles/layout/_init.css:
--------------------------------------------------------------------------------
1 | // NOTE - Init image background
2 | img.image-item {
3 | background-color: #fdfffc;
4 |
5 | &.--is-error {
6 | background: url('images/icons/image-loading-icon.png') center/contain
7 | no-repeat;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import ModuleSection from 'components/home-page/ModuleSection'
2 |
3 | function HomePage() {
4 | return (
5 |
6 |
7 |
8 | )
9 | }
10 |
11 | export default HomePage
12 |
--------------------------------------------------------------------------------
/config/utils/ObjectToEnvConverter.js:
--------------------------------------------------------------------------------
1 | module.exports = (obj) => {
2 | if (!obj || typeof obj !== 'object') return
3 |
4 | let tmpENVContent = ''
5 | for (let key in obj) {
6 | tmpENVContent += key + '=' + (!obj[key] ? '' : obj[key] + '\n')
7 | }
8 |
9 | return tmpENVContent
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/router/context/ValidationContext.ts:
--------------------------------------------------------------------------------
1 | export interface IValidation {
2 | error?: string
3 | status: 200 | 301 | 302 | 404 | 504
4 | redirect?: string | number
5 | }
6 |
7 | const INIT_VALIATION_INFO: IValidation | null = null
8 |
9 | export const ValidationContext = createContext(
10 | INIT_VALIATION_INFO
11 | )
12 |
--------------------------------------------------------------------------------
/index.production.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= htmlWebpackPlugin.options.title %>
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/app/router/hooks/useCertificateCustomizationInfo.ts:
--------------------------------------------------------------------------------
1 | import { IUserInfo, useUserInfo } from 'app/store/UserInfoContext'
2 |
3 | export interface ICertCustomizationInfo {
4 | user: IUserInfo
5 | }
6 |
7 | export default function useCertificateCustomizationInfo(): ICertCustomizationInfo {
8 | const { userState } = useUserInfo()
9 |
10 | return {
11 | user: userState,
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/CommentPage.tsx:
--------------------------------------------------------------------------------
1 | import CommentSection from 'components/comment-page/CommentSection'
2 | import CommentRow from 'components/comment-page/CommentRow'
3 |
4 | const Page = styled.div``
5 |
6 | function CommentPages() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
15 |
16 | export default CommentPages
17 |
--------------------------------------------------------------------------------
/config/commit-packages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "commit-packages",
3 | "version": "1.0.0",
4 | "description": "commit-packages",
5 | "main": "",
6 | "scripts": {},
7 | "author": "",
8 | "license": "ISC",
9 | "dependencies": {
10 | "@commitlint/config-conventional": "^17.6.6",
11 | "commitizen": "^4.3.0",
12 | "commitlint": "^17.6.6",
13 | "cz-git": "^1.6.1",
14 | "husky": "^8.0.3"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import 'assets/styles/main.css'
2 | import 'assets/styles/tailwind.css'
3 | import router from 'app/router/index'
4 | import { UserInfoProvider } from 'app/store/UserInfoContext'
5 |
6 | const root = createRoot(document.getElementById('root'))
7 |
8 | root.render(
9 |
10 |
11 |
12 |
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/src/app/router/utils/RouterInit.tsx:
--------------------------------------------------------------------------------
1 | import { RouteInitContext } from 'app/router/context/InfoContext'
2 |
3 | export default function RouterInit({ children }) {
4 | const location = useLocation()
5 | const matches = useMatches()
6 | const routeInit = matches.find((item) => item.pathname === location.pathname)
7 |
8 | return (
9 |
10 | {children}
11 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/env/env.router.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | prefix: 'router',
3 | data: {
4 | base: {
5 | path: '/',
6 | },
7 | home: {
8 | path: '/',
9 | },
10 | content: {
11 | path: ':slugs',
12 | },
13 | content_comment: {
14 | path: 'comment',
15 | },
16 | comment: {
17 | path: 'comment/detail',
18 | id: 'CommentPage',
19 | },
20 | login: {
21 | path: 'login',
22 | id: 'LoginPage',
23 | },
24 | not_found: {
25 | path: '*',
26 | },
27 | },
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/comment-page/CommentSection.tsx:
--------------------------------------------------------------------------------
1 | import LoadingBoundary from 'utils/LoadingBoundary'
2 | import CommentLoader from 'components/comment-page/CommentLoader'
3 |
4 | const Section = styled.section`
5 | margin-top: 24px;
6 | `
7 |
8 | export default function CommentSection({ children }) {
9 | const { id } = useRoute()
10 | return (
11 |
12 | }
15 | >
16 | {children}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/router/utils/LazyComponentHandler.tsx:
--------------------------------------------------------------------------------
1 | export function withLazy(
2 | f: () => Promise<{
3 | default: ComponentType
4 | }>
5 | ) {
6 | try {
7 | if (typeof f === 'function') {
8 | const Component = lazy(f)
9 | return
10 | } else {
11 | throw Object.assign(
12 | new Error(
13 | 'The param of withLazy function must be a Function return a Promise or a Dynamic Import that give a ComponentType'
14 | ),
15 | { code: 402 }
16 | )
17 | }
18 | } catch (err) {
19 | console.error(err)
20 | }
21 | } // withLazy
22 |
--------------------------------------------------------------------------------
/config/webpack.serve.config.js:
--------------------------------------------------------------------------------
1 | const { webpack } = require('webpack')
2 | const WebpackDevServer = require('webpack-dev-server')
3 | const { findFreePort } = require('./utils/PortHandler')
4 |
5 | ;(async () => {
6 | const port = await findFreePort(process.env.PORT)
7 | const serverInitial = new WebpackDevServer(
8 | webpack({
9 | mode: 'development',
10 | entry: {},
11 | output: {},
12 | }),
13 | {
14 | compress: true,
15 | port: port,
16 | static: './dist',
17 | historyApiFallback: true,
18 | }
19 | )
20 |
21 | serverInitial.start()
22 | })()
23 |
--------------------------------------------------------------------------------
/src/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import { Link as ReactLinkNative } from 'react-router-dom'
2 | import type { LinkProps } from 'react-router-dom'
3 |
4 | interface IProps extends LinkProps {}
5 |
6 | function Link(props: IProps) {
7 | const { children, ...linkProps } = props
8 |
9 | const onClick = (ev) => {
10 | if (ev.target.pathname === location.pathname) {
11 | ev.preventDefault()
12 | return
13 | }
14 | return true
15 | }
16 |
17 | return (
18 |
19 | {children}
20 |
21 | )
22 | }
23 |
24 | export default Link
25 |
--------------------------------------------------------------------------------
/config/lint-and-format-packages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lint-and-format-packages",
3 | "version": "1.0.0",
4 | "description": "lint-and-format-packages",
5 | "main": "",
6 | "scripts": {},
7 | "author": "",
8 | "license": "ISC",
9 | "dependencies": {
10 | "eslint": "^8.44.0",
11 | "eslint-config-airbnb": "^19.0.4",
12 | "eslint-config-airbnb-typescript": "^17.0.0",
13 | "eslint-config-prettier": "^8.8.0",
14 | "eslint-import-resolver-custom-alias": "^1.3.2",
15 | "eslint-plugin-import": "^2.27.5",
16 | "eslint-plugin-jsx-a11y": "^6.7.1",
17 | "eslint-plugin-prettier": "^4.2.1",
18 | "eslint-plugin-react": "^7.32.2",
19 | "eslint-plugin-react-hooks": "^4.6.0",
20 | "espree": "^9.6.0",
21 | "lint-staged": "^13.2.3",
22 | "prettier": "^2.8.8"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "nuxt.isNuxtApp": false,
3 |
4 | "path-autocomplete.extensionOnImport": true,
5 | "path-autocomplete.includeExtension": true,
6 | "path-autocomplete.excludedItems": {
7 | "**": {
8 | "when": "**/*.{js,ts,jsx,tsx}",
9 | "isDir": true,
10 | "context": "(import|require).*"
11 | }
12 | },
13 | "path-autocomplete.pathMappings": {
14 | "/": "${workspace}/src/assets/static",
15 | "assets": "${workspace}/src/assets"
16 | },
17 | "path-autocomplete.pathSeparators": " \t({[",
18 | "editor.suggest.showModules": false,
19 | "editor.formatOnSave": false,
20 | "editor.defaultFormatter": "esbenp.prettier-vscode",
21 | "editor.codeActionsOnSave": [
22 | "source.formatDocument",
23 | "source.fixAll.tslint",
24 | "source.fixAll.eslint"
25 | ],
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/ErrorBoundary.tsx:
--------------------------------------------------------------------------------
1 | export default class ErrorBoundary extends React.Component<{
2 | children?: any
3 | fallback?: any
4 | onError?: (error: any) => void
5 | timeout?: number
6 | }> {
7 | state = { hasError: false }
8 |
9 | static getDerivedStateFromError() {
10 | // Update state so the next render will show the fallback UI.
11 | return { hasError: true }
12 | }
13 |
14 | componentDidCatch(error) {
15 | this.props.onError?.(error)
16 | }
17 |
18 | render() {
19 | if (this.state.hasError) {
20 | // You can render any custom fallback UI
21 | const ErrorTemplate = this.props.fallback ? (
22 | this.props.fallback
23 | ) : (
24 | 'Something went wrong!'
25 | )
26 | return ErrorTemplate
27 | }
28 |
29 | return this.props.children
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/ImageItem.tsx:
--------------------------------------------------------------------------------
1 | const Image = styled.img`
2 | display: block;
3 | background-color: #fdfffc;
4 | width: 100%;
5 | height: 100%;
6 | object-fit: contain;
7 |
8 | &[src=''] {
9 | display: none;
10 | }
11 | `
12 |
13 | export const Outer = styled.div`
14 | height: 100px;
15 | width: 100%;
16 | &.--is-error {
17 | background: url('/images/icons/image-loading-icon.png') center/24px 24px
18 | no-repeat;
19 |
20 | & ${Image} {
21 | display: none;
22 | }
23 | }
24 | `
25 |
26 | function Component(props) {
27 | const [isError, setIsError] = useState(false)
28 |
29 | function onErrorHandler() {
30 | setIsError(true)
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | const ImageItem = memo(Component)
41 |
42 | export default ImageItem
43 |
--------------------------------------------------------------------------------
/src/app/store/UserInfoContext.tsx:
--------------------------------------------------------------------------------
1 | export type IUserInfo = {
2 | email: string
3 | }
4 |
5 | const INIT_USER_INFO: IUserInfo = { email: '' }
6 |
7 | export const UserInfoContext = createContext<{
8 | userState: IUserInfo
9 | setUserState: Dispatch>
10 | }>({
11 | userState: INIT_USER_INFO,
12 | setUserState: () => null,
13 | })
14 |
15 | export function UserInfoProvider({ children }) {
16 | const [userState, setUserState] = useReducer(
17 | (currentData, updateData) => ({
18 | ...currentData,
19 | ...updateData,
20 | }),
21 | INIT_USER_INFO
22 | )
23 |
24 | return (
25 |
31 | {children}
32 |
33 | )
34 | } // UserInfoDeliver()
35 |
36 | export function useUserInfo() {
37 | return useContext(UserInfoContext)
38 | } // useUserInfo()
39 |
--------------------------------------------------------------------------------
/src/components/content-page/ModuleContentSection.tsx:
--------------------------------------------------------------------------------
1 | import ImageItem, { Outer as ImageOuter } from 'components/ImageItem'
2 |
3 | const Section = styled.section`
4 | ${ImageOuter} {
5 | height: 200px;
6 | max-width: 320px;
7 | margin: 0 auto;
8 | border: 1px solid ${import.meta.env.STYLE_COLOR_DARK};
9 | }
10 | `
11 | const Caption = styled.div`
12 | font-size: 12px;
13 | color: ${rgba(import.meta.env.STYLE_COLOR_DARK, 0.5)};
14 | margin-top: 8px;
15 | `
16 | const Content = styled.div`
17 | margin-top: 16px;
18 | `
19 |
20 | export default function ModuleContentSection({
21 | src,
22 | caption,
23 | content,
24 | }: {
25 | src?: string
26 | caption: string
27 | content: string
28 | }) {
29 | console.log('rerendered ModuleContentSection')
30 | return (
31 |
32 |
33 | {caption}
34 | {content}
35 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/src/utils/CookieHelper.ts:
--------------------------------------------------------------------------------
1 | export function getCookie(cname) {
2 | let name = cname + '='
3 | let decodedCookie = decodeURIComponent(document.cookie)
4 | let ca = decodedCookie.split(';')
5 | for (let i = 0; i < ca.length; i++) {
6 | let c = ca[i]
7 | while (c.charAt(0) == ' ') {
8 | c = c.substring(1)
9 | }
10 | if (c.indexOf(name) == 0) {
11 | return c.substring(name.length, c.length)
12 | }
13 | }
14 | return ''
15 | } // getCookie
16 |
17 | export function setCookie(cname, cvalue, exdays?) {
18 | let d
19 | let expires
20 |
21 | if (exdays) {
22 | d = new Date()
23 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
24 | expires = 'expires=' + d.toUTCString()
25 | }
26 |
27 | document.cookie =
28 | cname + '=' + cvalue + (expires ? ';' + expires : '') + ';path=/'
29 | } // setCookie
30 |
31 | export function deleteCookie(cname) {
32 | document.cookie = cname + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;'
33 | } // deleteCookie
34 |
--------------------------------------------------------------------------------
/src/static.d.ts:
--------------------------------------------------------------------------------
1 | import type { IndexRouteObject } from 'react-router-dom'
2 |
3 | declare module '*.svg' {
4 | const value: string
5 | export = value
6 | }
7 | declare module '*.png' {
8 | const value: string
9 | export = value
10 | }
11 | declare module '*.jpg' {
12 | const value: string
13 | export = value
14 | }
15 | declare module '*.jpeg' {
16 | const value: string
17 | export = value
18 | }
19 | declare module '*.webp' {
20 | const value: string
21 | export = value
22 | }
23 | declare module '*.mp3' {
24 | const value: string
25 | export = value
26 | }
27 | declare module '*.mp4' {
28 | const value: string
29 | export = value
30 | }
31 | declare module '*.mov' {
32 | const value: string
33 | export = value
34 | }
35 | declare module '*.mkv' {
36 | const value: string
37 | export = value
38 | }
39 | declare module '*.webm' {
40 | const value: string
41 | export = value
42 | }
43 | declare module '*.scss' {
44 | const value: string
45 | export = value
46 | }
47 |
--------------------------------------------------------------------------------
/src/utils/LoadingBoundary.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactElement, ReactNode, MutableRefObject } from 'react'
2 |
3 | function useWithDelay(delay: number, fallback: ReactNode): ReactNode {
4 | const [isShow, setIsShow] = useState(delay === 0 ? true : false)
5 |
6 | const timeout: MutableRefObject = useRef(null)
7 |
8 | useEffect(() => {
9 | if (!isShow) {
10 | timeout.current = setTimeout(function () {
11 | setIsShow(true)
12 | }, delay)
13 | }
14 | }, [delay, isShow])
15 |
16 | return isShow ? fallback : ''
17 | }
18 |
19 | export default function LoadingBoundary({
20 | children,
21 | delay,
22 | fallback,
23 | }: {
24 | children?: ReactNode | undefined
25 | delay?: number
26 | fallback?: ReactNode
27 | }): ReactElement {
28 | const delayTime: number = Number(delay) || 0
29 |
30 | const Component: ReactNode = useWithDelay(delayTime, fallback)
31 |
32 | return {children}
33 | } // LoadingBoundary
34 |
--------------------------------------------------------------------------------
/config/types/ImportMeta.d.ts:
--------------------------------------------------------------------------------
1 | interface ImportMeta {
2 | env: Env;
3 | }
4 |
5 | interface Env {
6 | PORT: number;
7 | IO_PORT: number;
8 | LOCAL_ADDRESS: string;
9 | LOCAL_HOST: string;
10 | IPV4_ADDRESS: string;
11 | IPV4_HOST: string;
12 | IO_HOST: string;
13 | ROUTER_BASE_PATH: string;
14 | ROUTER_HOME_PATH: string;
15 | ROUTER_CONTENT_PATH: string;
16 | ROUTER_CONTENT_COMMENT_PATH: string;
17 | ROUTER_COMMENT_PATH: string;
18 | ROUTER_COMMENT_ID: string;
19 | ROUTER_LOGIN_PATH: string;
20 | ROUTER_LOGIN_ID: string;
21 | ROUTER_NOT_FOUND_PATH: string;
22 | STYLE_COLOR_DARK: string;
23 | STYLE_COLOR_YELLOW: string;
24 | STYLE_COLOR_BLUE: string;
25 | STYLE_COLOR_WHITE: string;
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/router/types.ts:
--------------------------------------------------------------------------------
1 | import { IndexRouteObject } from 'react-router-dom'
2 | import { ICertCustomizationInfo } from './hooks/useCertificateCustomizationInfo'
3 | import { ICertInfo } from './utils/RouterProtection'
4 | import { IRouteInfo } from './context/InfoContext'
5 |
6 | export interface RouteObjectCustomize
7 | extends Omit {
8 | index?: boolean
9 | handle?: {
10 | params?: {
11 | validate?: (params: Record) => boolean
12 | [key: string]: any
13 | }
14 | protect?: (
15 | certInfo: ICertInfo & ICertCustomizationInfo,
16 | route: IRouteInfo
17 | ) => boolean | string
18 | reProtect?: (
19 | certInfo: ICertInfo & ICertCustomizationInfo,
20 | route: IRouteInfo
21 | ) => boolean | string
22 | [key: string]: any
23 | }
24 | children?: RouteObjectCustomize[]
25 | }
26 |
27 | export interface IRouterProtectionProps {
28 | waitingVerifyRouterIDList?: { [key: string]: Array }
29 | children?: ReactNode
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/router/context/LoadingInfoContext.tsx:
--------------------------------------------------------------------------------
1 | export type ILoadingInfo = {
2 | isShow: boolean
3 | element: JSX.Element
4 | }
5 |
6 | export const INIT_LOADING_INFO: ILoadingInfo = {
7 | isShow: false,
8 | element: <>>,
9 | }
10 |
11 | export const LoadingInfoContext = createContext<{
12 | loadingState: ILoadingInfo
13 | setLoadingState: Dispatch>
14 | }>({
15 | loadingState: INIT_LOADING_INFO,
16 | setLoadingState: () => null,
17 | })
18 |
19 | export function LoadingInfoProvider({ children }) {
20 | const [loadingState, setLoadingState] = useReducer(
21 | (currentData, updateData) => ({
22 | ...currentData,
23 | ...updateData,
24 | }),
25 | INIT_LOADING_INFO
26 | )
27 |
28 | return (
29 |
35 | {children}
36 |
37 | )
38 | } // LoadingInfoProvider
39 |
40 | export function useLoadingInfo() {
41 | return useContext(LoadingInfoContext)
42 | } // useLoadingInfo
43 |
--------------------------------------------------------------------------------
/config/utils/StyleLoader.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const loader = require('style-loader')
3 |
4 | module.exports = function () {}
5 |
6 | module.exports.pitch = function (request) {
7 | const result = loader.pitch.call(this, request)
8 | const index = result.indexOf(
9 | 'options.styleTagTransform = styleTagTransformFn;\n'
10 | )
11 | if (index === -1) return result
12 | const insertIndex = index - 1
13 |
14 | // eslint-disable-next-line prefer-destructuring
15 | const resourcePath = this.resourcePath
16 | const relativePath = path.relative(
17 | path.resolve(__dirname, '..'),
18 | resourcePath
19 | )
20 |
21 | const insertAttr = `
22 | if (typeof options.attributes !== 'object') {
23 | options.attributes = {}
24 | }
25 | options.attributes["source-path"] = '${relativePath}' // do anything you want
26 | runtimeOptions.attributes = options.attributes;
27 | `
28 |
29 | console.log(
30 | result.slice(0, insertIndex) + insertAttr + result.slice(insertIndex)
31 | )
32 |
33 | return result.slice(0, insertIndex) + insertAttr + result.slice(insertIndex)
34 | }
35 |
--------------------------------------------------------------------------------
/config/utils/SocketHandler.js:
--------------------------------------------------------------------------------
1 | const http = require('http')
2 | const { Server } = require('socket.io')
3 | const { findFreePort, setPort } = require('./PortHandler')
4 |
5 | const server = http.createServer()
6 | const io = new Server(server)
7 |
8 | let socket = null
9 | const promiseIOConnection = new Promise(async (resolve) => {
10 | const SOCKET_IO_PORT = await findFreePort(3030)
11 | setPort(SOCKET_IO_PORT, 'SOCKET_IO_PORT')
12 |
13 | let callback = null
14 | io.on('connection', (initializeSocket) => {
15 | let sockets = {}
16 | // Save the list of all connections to a variable
17 | sockets[initializeSocket.io] = initializeSocket
18 | socket = initializeSocket
19 |
20 | // When disconnect, delete the socket with the variable
21 | initializeSocket.on('disconnect', () => {
22 | delete sockets[initializeSocket.id]
23 | })
24 |
25 | if (callback) {
26 | callback({ server, io, socket })
27 | } else {
28 | resolve({
29 | server,
30 | io,
31 | socket,
32 | setupCallback: function (fn) {
33 | callback = fn
34 | },
35 | })
36 | }
37 | })
38 |
39 | server.listen(SOCKET_IO_PORT)
40 | })
41 |
42 | module.exports = promiseIOConnection
43 |
--------------------------------------------------------------------------------
/config/utils/NetworkGenerator.js:
--------------------------------------------------------------------------------
1 | //=======================================
2 | // NOTE - generate External IP
3 | //=======================================
4 | const os = require('os')
5 | const OSNetworkInterfaces = os.networkInterfaces()
6 | const Ethernet =
7 | OSNetworkInterfaces.Ethernet || Object.values(OSNetworkInterfaces)
8 |
9 | let IPV4_ADDRESS = null
10 |
11 | if (Ethernet) {
12 | let ethernetFormatted = Ethernet
13 |
14 | if (ethernetFormatted[0].length) {
15 | let tmpEthernetFormatted = []
16 |
17 | ethernetFormatted.forEach(function (EthernetItem) {
18 | tmpEthernetFormatted = tmpEthernetFormatted.concat(EthernetItem)
19 | })
20 |
21 | ethernetFormatted = tmpEthernetFormatted
22 | }
23 |
24 | ethernetFormatted.some(function (ethernetItem) {
25 | const ethernetItemInfo =
26 | ethernetItem.family &&
27 | /ipv4|4/.test(ethernetItem.family?.toLowerCase?.() ?? ethernetItem.family)
28 | ? ethernetItem
29 | : null
30 |
31 | if (
32 | ethernetItemInfo &&
33 | ethernetItemInfo.address !== '127.0.0.1' &&
34 | (ethernetItemInfo.address.includes('192') ||
35 | ethernetItemInfo.address.includes('172'))
36 | ) {
37 | IPV4_ADDRESS = ethernetItemInfo.address
38 | return true
39 | }
40 | })
41 | }
42 |
43 | module.exports = { IPV4_ADDRESS }
44 |
--------------------------------------------------------------------------------
/config/types/dts-generator.mjs:
--------------------------------------------------------------------------------
1 | import {
2 | quicktype,
3 | InputData,
4 | jsonInputForTargetLanguage,
5 | } from 'quicktype-core'
6 | import {
7 | ENV_OBJECT_DEFAULT as ImportMeta,
8 | promiseENVWriteFileSync,
9 | } from '../env/env.mjs'
10 | import { writeFile } from 'fs'
11 | import { resolve } from 'path'
12 |
13 | async function quicktypeJSON(targetLanguage, jsonString) {
14 | const jsonInput = jsonInputForTargetLanguage(targetLanguage)
15 |
16 | // We could add multiple samples for the same desired
17 | // type, or many sources for other types. Here we're
18 | // just making one type from one piece of sample JSON.
19 | await jsonInput.addSource({
20 | name: 'ImportMeta',
21 | samples: [jsonString],
22 | })
23 |
24 | const inputData = new InputData()
25 | inputData.addInput(jsonInput)
26 |
27 | return await quicktype({
28 | inputData,
29 | lang: targetLanguage,
30 | rendererOptions: { 'just-types': 'true' },
31 | })
32 | }
33 |
34 | async function main() {
35 | const { lines: tdsGroup } = await quicktypeJSON(
36 | 'typescript',
37 | JSON.stringify({ env: ImportMeta })
38 | )
39 |
40 | writeFile(
41 | resolve('./config/types', 'ImportMeta.d.ts'),
42 | tdsGroup.join('\n').replace(/export\s/g, ''),
43 | function (err) {}
44 | )
45 | }
46 |
47 | main()
48 |
49 | export { promiseENVWriteFileSync }
50 |
--------------------------------------------------------------------------------
/src/hooks/useStringHelper.ts:
--------------------------------------------------------------------------------
1 | import {
2 | generateSentenceCase,
3 | generateTitleCase,
4 | getSlug,
5 | getSlugWithoutDash,
6 | getUnsignedLetters,
7 | } from 'utils/StringHelper'
8 |
9 | export const useSlug = () => {
10 | const [state, set] = useState()
11 | const setState = (param: string) => {
12 | set(getSlug(param))
13 | }
14 |
15 | return [state, setState]
16 | } // useSlug
17 |
18 | export const useSlugWithoutDash = () => {
19 | const [state, set] = useState()
20 | const setState = (param: string) => {
21 | set(getSlugWithoutDash(param))
22 | }
23 |
24 | return [state, setState]
25 | } // useSlugWithoutDash
26 |
27 | export const useUnsignedLetters = () => {
28 | const [state, set] = useState()
29 | const setState = (param: string) => {
30 | set(getUnsignedLetters(param))
31 | }
32 |
33 | return [state, setState]
34 | } // useUnsignedLetters
35 |
36 | export const useTitleCase = () => {
37 | const [state, set] = useState()
38 | const setState = (param: string) => {
39 | set(generateTitleCase(param))
40 | }
41 |
42 | return [state, setState]
43 | } // useTitleCase
44 |
45 | export const useSentenceCase = () => {
46 | const [state, set] = useState()
47 | const setState = (param: string) => {
48 | set(generateSentenceCase(param))
49 | }
50 |
51 | return [state, setState]
52 | } // useSentenceCase
53 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/diamond.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
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 |
--------------------------------------------------------------------------------
/src/pages/LoginPage.tsx:
--------------------------------------------------------------------------------
1 | import { useUserInfo } from 'app/store/UserInfoContext'
2 | import ImageItem, { Outer as ImageOuter } from 'components/ImageItem'
3 |
4 | const Page = styled.div``
5 |
6 | const Section = styled.section`
7 | height: 100vh;
8 | `
9 |
10 | const Block = styled.div`
11 | position: relative;
12 | display: flex;
13 | max-width: 320px;
14 | flex-wrap: wrap;
15 | justify-content: center;
16 | left: 50%;
17 | top: 30%;
18 | transform: translate(-50%);
19 | `
20 |
21 | const Avatar = styled.div`
22 | height: 100px;
23 | flex: 0 0 100px;
24 | ${ImageOuter} {
25 | height: 100%;
26 | width: 100%;
27 | overflow: hidden;
28 | border-radius: 50%;
29 | background-color: ${rgba(import.meta.env.STYLE_COLOR_DARK, 0.1)};
30 | background-size: 16px 16px;
31 | }
32 | `
33 |
34 | const Button = styled.button`
35 | margin-top: 16px;
36 | width: 100%;
37 | cursor: pointer;
38 | `
39 |
40 | export default function LoginPage() {
41 | const route = useRoute()
42 | const { setUserState } = useUserInfo()
43 |
44 | const onClickLogin = () => {
45 | setUserState({ email: 'abc@gmail.com' })
46 | }
47 |
48 | return (
49 |
50 |
51 |
52 | {'< Back to HomePage'}
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | Login
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/config/build-tool-packages/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "build-tool-packages",
3 | "version": "1.0.0",
4 | "description": "build-tool-packages",
5 | "main": "",
6 | "scripts": {},
7 | "author": "",
8 | "license": "ISC",
9 | "overrides": {
10 | "isomorphic-fetch@*": "$isomorphic-fetch"
11 | },
12 | "dependencies": {
13 | "@babel/core": "7.22.15",
14 | "@babel/preset-env": "^7.22.15",
15 | "@babel/preset-react": "7.22.15",
16 | "@babel/preset-typescript": "7.22.15",
17 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
18 | "@types/react": "^18.2.14",
19 | "@types/webpack-env": "^1.18.1",
20 | "autoprefixer": "^10.4.14",
21 | "babel-loader": "^9.1.2",
22 | "clean-webpack-plugin": "^4.0.0",
23 | "copy-webpack-plugin": "^11.0.0",
24 | "core-js": "^3.31.0",
25 | "cross-env": "^7.0.3",
26 | "css-loader": "^6.8.1",
27 | "css-minimizer-webpack-plugin": "^5.0.1",
28 | "html-webpack-plugin": "^5.5.3",
29 | "isomorphic-fetch": "^3.0.0",
30 | "mini-css-extract-plugin": "^2.7.6",
31 | "postcss": "^8.4.24",
32 | "postcss-loader": "^7.3.3",
33 | "postcss-preset-env": "^9.0.0",
34 | "postcss-simple-vars": "^7.0.1",
35 | "purgecss-webpack-plugin": "^5.0.0",
36 | "quicktype-core": "^23.0.49",
37 | "react-refresh": "^0.14.0",
38 | "socket.io": "^4.7.1",
39 | "esbuild-loader": "^4.0.2",
40 | "tailwindcss": "^3.3.2",
41 | "terser-webpack-plugin": "^5.3.9",
42 | "unplugin-auto-import": "^0.16.5",
43 | "webpack": "^5.88.1",
44 | "webpack-cli": "^5.1.4",
45 | "webpack-dev-server": "^4.15.1"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/components/comment-page/CommentLoader.tsx:
--------------------------------------------------------------------------------
1 | import ImageItem, { Outer as ImageOuter } from 'components/ImageItem'
2 |
3 | const Row = styled.div`
4 | display: flex;
5 | margin-bottom: 24px;
6 |
7 | &:last-child {
8 | margin-bottom: 0;
9 | }
10 | `
11 | const AvatarCol = styled.div`
12 | margin-right: 8px;
13 | flex: 0 0 50px;
14 | height: 50px;
15 | ${ImageOuter} {
16 | height: 100%;
17 | width: 100%;
18 | overflow: hidden;
19 | border-radius: 50%;
20 | background-color: ${rgba(import.meta.env.STYLE_COLOR_DARK, 0.1)};
21 | background-size: 16px 16px;
22 | }
23 | `
24 | const MetaCol = styled.div`
25 | min-width: 0;
26 | flex: 1 1 auto;
27 | `
28 | const NameLabel = styled.p`
29 | width: 25%;
30 | height: 14px;
31 | background: ${rgba(import.meta.env.STYLE_COLOR_DARK, 0.1)};
32 | margin-bottom: 8px;
33 | `
34 | const ContentLabel = styled.div`
35 | width: 50%;
36 | height: 14px;
37 | margin-bottom: 4px;
38 | background: ${rgba(import.meta.env.STYLE_COLOR_DARK, 0.1)};
39 |
40 | &:last-child {
41 | margin-bottom: 0;
42 | }
43 |
44 | &:nth-of-type(2) {
45 | width: 40%;
46 | }
47 | `
48 |
49 | export default function CommentLoader({ amount }: { amount: number }) {
50 | const commentList = new Array(amount).fill(null).map((val, idx) => (
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | ))
62 | return <>{commentList}>
63 | }
64 |
--------------------------------------------------------------------------------
/src/Layout.tsx:
--------------------------------------------------------------------------------
1 | import ErrorBoundary from 'utils/ErrorBoundary'
2 | import LoadingBoundary from 'utils/LoadingBoundary'
3 | import LoadingPageComponent from 'components/LoadingPageComponent'
4 | import ErrorLoadingPageComponent from 'components/ErrorPageComponent'
5 | import { useUserInfo } from 'app/store/UserInfoContext'
6 |
7 | const MainContainer = styled.div`
8 | max-width: 1280px;
9 | min-width: 0;
10 | min-height: 100vh;
11 | overflow: hidden;
12 | padding: 16px;
13 | margin: 0 auto;
14 | `
15 |
16 | const Header = styled.header`
17 | padding: 16px;
18 | text-align: right;
19 | `
20 |
21 | function Layout() {
22 | const route = useRoute()
23 | const { userState, setUserState } = useUserInfo()
24 |
25 | const onClickLogout = () => {
26 | setUserState({ email: '' })
27 | }
28 |
29 | return (
30 |
31 |
32 |
33 | {userState && userState.email ? (
34 | <>
35 | {userState.email + ' | '}
36 |
37 | Logout
38 |
39 | >
40 | ) : (
41 |
45 | Login
46 |
47 | )}
48 |
49 | }>
50 | }
54 | >
55 |
56 |
57 |
58 |
59 |
60 | )
61 | } // App()
62 |
63 | export default Layout
64 |
--------------------------------------------------------------------------------
/src/components/LoadingPageComponent.tsx:
--------------------------------------------------------------------------------
1 | const RotationAnimation = keyframes`
2 | 0% {
3 | transform: rotate(0deg);
4 | }
5 | 100% {
6 | transform: rotate(360deg);
7 | }
8 | `
9 |
10 | const Wrapper = styled.div`
11 | position: fixed;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | z-index: 100;
16 | width: 100%;
17 | height: 100%;
18 | top: 0;
19 | left: 0;
20 | `
21 |
22 | const Loading = styled.span`
23 | width: 175px;
24 | height: 80px;
25 | display: block;
26 | margin: auto;
27 | background-image: radial-gradient(
28 | circle 25px at 25px 25px,
29 | #efefef 100%,
30 | transparent 0
31 | ),
32 | radial-gradient(circle 50px at 50px 50px, #efefef 100%, transparent 0),
33 | radial-gradient(circle 25px at 25px 25px, #efefef 100%, transparent 0),
34 | linear-gradient(#efefef 50px, transparent 0);
35 | background-size: 50px 50px, 100px 76px, 50px 50px, 120px 40px;
36 | background-position: 0px 30px, 37px 0px, 122px 30px, 25px 40px;
37 | background-repeat: no-repeat;
38 | position: relative;
39 | box-sizing: border-box;
40 | &:after {
41 | content: '';
42 | left: 0;
43 | right: 0;
44 | margin: auto;
45 | bottom: 20px;
46 | position: absolute;
47 | width: 36px;
48 | height: 36px;
49 | border-radius: 50%;
50 | border: 5px solid transparent;
51 | border-color: #ff3d00 transparent;
52 | box-sizing: border-box;
53 | animation: ${RotationAnimation} 1s linear infinite;
54 | }
55 | `
56 |
57 | function Component() {
58 | return (
59 |
60 |
61 |
62 | )
63 | }
64 |
65 | const LoadingPageComponent = memo(Component)
66 |
67 | export default LoadingPageComponent
68 |
--------------------------------------------------------------------------------
/config/eslint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | ignorePatterns: ['webpack.config.js', 'env/**/*', 'config/**/*', 'dist/**/*'],
4 | extends: [
5 | '.eslintrc-auto-import.json',
6 | 'airbnb-typescript',
7 | 'airbnb/hooks',
8 | 'plugin:@typescript-eslint/recommended',
9 | // 'plugin:jest/recommended',
10 | 'plugin:prettier/recommended',
11 | 'eslint:recommended',
12 | 'plugin:import/recommended',
13 | 'plugin:import/typescript',
14 | 'plugin:import/errors',
15 | 'plugin:import/warnings',
16 | 'prettier',
17 | ],
18 | plugins: ['react', '@typescript-eslint/eslint-plugin'],
19 | env: {
20 | browser: true,
21 | es6: true,
22 | node: true,
23 | // jest: true,
24 | },
25 | globals: {
26 | Atomics: 'readonly',
27 | SharedArrayBuffer: 'readonly',
28 | NodeJS: true,
29 | },
30 | parser: '@typescript-eslint/parser',
31 | parserOptions: {
32 | parser: {
33 | js: 'espree',
34 | jsx: 'espree',
35 | '': 'espree',
36 | },
37 | ecmaFeatures: {
38 | jsx: true,
39 | tsx: true,
40 | },
41 | ecmaVersion: 'latest',
42 | sourceType: 'module',
43 | project: './tsconfig.json',
44 | },
45 | rules: {
46 | 'linebreak-style': 'off',
47 | 'prettier/prettier': [
48 | 'error',
49 | {
50 | endOfLine: 'auto',
51 | },
52 | ],
53 | '@typescript-eslint/naming-convention': 'off',
54 | 'no-unused-vars': 'warn',
55 | 'react-hooks/rules-of-hooks': 'warn',
56 | 'react-hooks/exhaustive-deps': 'warn',
57 | },
58 | settings: {
59 | 'import/resolver': {
60 | 'eslint-import-resolver-custom-alias': {
61 | alias: {
62 | '': './src',
63 | },
64 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
65 | },
66 | },
67 | },
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/home-page/ModuleSection.tsx:
--------------------------------------------------------------------------------
1 | import ImageItem, { Outer as ImageOuter } from 'components/ImageItem'
2 |
3 | // NOTE - Dummy Data Region
4 | const moduleList: Array<{
5 | title: string
6 | id: number
7 | isVip: boolean
8 | }> = [
9 | {
10 | title: 'Excepteur nostrud deserunt do ipsum eu dolore.',
11 | id: 1,
12 | isVip: false,
13 | },
14 | {
15 | title:
16 | 'Ex Lorem commodo nisi et qui adipisicing consectetur magna duis enim pariatur eu.',
17 | id: 2,
18 | isVip: false,
19 | },
20 | {
21 | title: 'Dolor in voluptate anim magna.',
22 | id: 3,
23 | isVip: false,
24 | },
25 | {
26 | title:
27 | 'Eiusmod exercitation sint adipisicing magna sit dolore adipisicing.',
28 | id: 4,
29 | isVip: true,
30 | },
31 | ]
32 | // NOTE - End Dummy Data Region
33 |
34 | // NOTE - Styled Components Region
35 | const ModuleCard = styled.div`
36 | ${ImageOuter} {
37 | border-radius: 8px;
38 | border: 1px solid ${import.meta.env.STYLE_COLOR_DARK};
39 | background-color: ${import.meta.env.STYLE_COLOR_WHITE};
40 | }
41 | `
42 | const Title = styled.div`
43 | color: ${import.meta.env.STYLE_COLOR_DARK};
44 | text-decoration: none;
45 | max-width: 100%;
46 | white-space: nowrap;
47 | overflow: hidden;
48 | text-overflow: ellipsis;
49 | margin-top: 8px;
50 | `
51 | // NOTE - End Styled Components Region
52 |
53 | export default function ModuleSection() {
54 | const cardList = moduleList.map((item) => (
55 |
56 |
57 |
58 | {item.title}
59 |
60 |
61 | ))
62 |
63 | return {cardList}
64 | }
65 |
--------------------------------------------------------------------------------
/src/utils/Suspender.ts:
--------------------------------------------------------------------------------
1 | export function Suspender() {
2 | let _reject: (err: any) => void
3 | let _resolve: (result: any) => void
4 | let _suspender
5 | let _result
6 | const _resetAfterFinally = (() => {
7 | let timeout = null
8 | return () => {
9 | if (timeout) {
10 | clearTimeout(timeout)
11 | timeout = null
12 | }
13 | timeout = setTimeout(() => {
14 | timeout = null
15 | _suspender = undefined
16 | _resolve = () => {}
17 | _reject = () => {}
18 | }, 5)
19 | }
20 | })() // _resetAfterFinally()
21 |
22 | const _reset = (() => {
23 | let timeout = null
24 | return () =>
25 | new Promise((res) => {
26 | if (timeout) {
27 | clearTimeout(timeout)
28 | timeout = null
29 | }
30 | timeout = setTimeout(() => {
31 | timeout = null
32 | _suspender = undefined
33 | _result = undefined
34 | _resolve = () => {}
35 | _reject = () => {}
36 | res(null)
37 | }, 5)
38 | })
39 | })() // _reset()
40 |
41 | const _start = (f?: () => void) => {
42 | if (!_suspender) {
43 | _result = undefined
44 | _suspender = new Promise((resolve, reject) => {
45 | _resolve = (data) => {
46 | resolve(data)
47 | _resetAfterFinally()
48 | _result = data
49 | }
50 | _reject = (err) => {
51 | reject(err)
52 | _result = err
53 | }
54 |
55 | f?.()
56 | })
57 | }
58 | } // _start()
59 |
60 | const _get = () => {
61 | if (_result) return _result
62 | if (_suspender) throw _suspender
63 | return
64 | } // _get()
65 |
66 | return {
67 | start: _start,
68 | reject: (err) => _reject?.(err),
69 | resolve: (result) => _resolve?.(result),
70 | get: _get,
71 | reset: _reset,
72 | }
73 | } // Suspender
74 |
--------------------------------------------------------------------------------
/config/cz.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | useEmoji: true,
3 | skipQuestions: ['footerPrefix', 'footer', 'confirmCommit'],
4 | types: [
5 | {
6 | value: 'chore',
7 | name: "chore: 🤷 If you don't know the type will select",
8 | emoji: ':shrug:',
9 | },
10 | {
11 | value: 'perf',
12 | name: 'perf: ⚡️ Improve perfomance',
13 | emoji: ':zap:',
14 | },
15 | {
16 | value: 'release',
17 | name: 'release: 🎯 Create a release commit',
18 | emoji: ':dart:',
19 | },
20 | {
21 | value: 'docs',
22 | name: 'docs: 🗒 Create / change some document files (ex: *.docs, *.md)',
23 | emoji: ':spiral_notepad:',
24 | },
25 | {
26 | value: 'test',
27 | name: 'test: 🔬 Add / change a test',
28 | emoji: ':microscope:',
29 | },
30 | {
31 | value: 'style',
32 | name: 'style: 🎨 Only style for layout',
33 | emoji: ':art:',
34 | },
35 | {
36 | value: 'fix',
37 | name: 'fix: 🐞 Fix a bug',
38 | emoji: ':lady_beetle:',
39 | },
40 | {
41 | value: 'feat',
42 | name: 'feat: 🧩 Create a new feature',
43 | emoji: ':jigsaw:',
44 | },
45 | {
46 | value: 'update',
47 | name: 'update: 🧩 Update but not improve performance',
48 | emoji: ':jigsaw:',
49 | },
50 | ],
51 | scopes: [
52 | 'page',
53 | 'comp-page',
54 | 'comp-glob',
55 | 'lib',
56 | 'util',
57 | 'enum',
58 | 'define',
59 | 'server',
60 | 'other',
61 | ],
62 | messages: {
63 | type: 'Select the type of committing:',
64 | customScope: 'Select the scope this component affects:',
65 | subject: 'Title:\n',
66 | body: 'Description:\n',
67 | breaking: 'List any breaking changes:\n',
68 | footer: 'Issues this commit closes, e.g #123:',
69 | confirmCommit: 'Ready to commit ?\n',
70 | },
71 | }
72 |
--------------------------------------------------------------------------------
/src/app/router/utils/RouterValidation.tsx:
--------------------------------------------------------------------------------
1 | import type { Params } from 'react-router'
2 | import type { IValidation } from 'app/router/context/ValidationContext'
3 |
4 | function useValidateBasicParam(): IValidation {
5 | const params = useParams()
6 |
7 | for (const key in params) {
8 | if (
9 | params[key] &&
10 | (!/^[a-zA-Z0-9._-]+$/.test(params[key] as string) ||
11 | /[._-]+$/.test(params[key] as string))
12 | ) {
13 | return {
14 | status: 404,
15 | }
16 | }
17 | }
18 |
19 | return {
20 | status: 200,
21 | }
22 | } //ValidateBasicParam()
23 |
24 | function useValidateCustomParams(): IValidation {
25 | const params = useParams()
26 |
27 | const validation: IValidation = {
28 | status: 200,
29 | }
30 |
31 | const matches = useMatches()
32 | matches.some(function (item) {
33 | const validate = (
34 | item as {
35 | handle?: {
36 | params?: {
37 | validate: (params: Params) => IValidation
38 | }
39 | }
40 | }
41 | )?.handle?.params?.validate
42 |
43 | if (params && typeof validate === 'function' && !validate(params)) {
44 | validation.status = 404
45 | return true
46 | }
47 | })
48 |
49 | return validation
50 | } // ValidateCustomParams()
51 |
52 | export default function RouterValidation({ children, NotFoundPage }) {
53 | const validation = (() => {
54 | const validationList = [useValidateBasicParam, useValidateCustomParams]
55 |
56 | for (const key in validationList) {
57 | if (typeof validationList[key] !== 'function') continue
58 |
59 | const result = validationList[key]()
60 |
61 | if (result && result.status === 404) {
62 | return result
63 | }
64 | }
65 |
66 | return { status: 200 }
67 | })()
68 |
69 | if (validation.status === 404) {
70 | return
71 | }
72 |
73 | return children
74 | } // RouterValidation()
75 |
--------------------------------------------------------------------------------
/config/.git-cz.json:
--------------------------------------------------------------------------------
1 | {
2 | "list": [
3 | "feat",
4 | "style",
5 | "test",
6 | "fix",
7 | "docs",
8 | "release",
9 | "update",
10 | "perf",
11 | "chore"
12 | ],
13 | "questions": ["type", "scope", "subject", "body", "breaking", "issues"],
14 | "types": {
15 | "chore": {
16 | "description": "If you don't know the type will select",
17 | "emoji": "🤷",
18 | "value": "chore"
19 | },
20 | "perf": {
21 | "description": "Improve perfomance",
22 | "emoji": "⚡️",
23 | "value": "perf"
24 | },
25 | "release": {
26 | "description": "Create a release commit",
27 | "emoji": "🎯",
28 | "value": "release"
29 | },
30 | "docs": {
31 | "description": " Create / change some document files (ex: *.docs, *.md)",
32 | "emoji": "🗒",
33 | "value": " docs"
34 | },
35 | "test": {
36 | "description": "Add / change a test",
37 | "emoji": "🔬",
38 | "value": "test"
39 | },
40 | "style": {
41 | "description": "Only style for layout",
42 | "emoji": "🎨",
43 | "value": "style"
44 | },
45 | "fix": {
46 | "description": "Fix a bug",
47 | "emoji": "🐞",
48 | "value": "fix"
49 | },
50 | "feat": {
51 | "description": "Create a new feature",
52 | "emoji": "🧩",
53 | "value": "feat"
54 | },
55 | "update": {
56 | "description": "Update but not improve performance",
57 | "emoji": "🧩",
58 | "value": "update"
59 | }
60 | },
61 | "scopes": [
62 | "page",
63 | "comp-page",
64 | "comp-glob",
65 | "lib",
66 | "util",
67 | "enum",
68 | "define",
69 | "server",
70 | "other"
71 | ],
72 | "messages": {
73 | "type": "Select the type of committing:",
74 | "customScope": "Select the scope this component affects:",
75 | "subject": "Title:\n",
76 | "body": "Description:\n",
77 | "breaking": "List any breaking changes:\n",
78 | "footer": "Issues this commit closes, e.g #123:",
79 | "confirmCommit": "Ready to commit ?\n"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/router/context/InfoContext.ts:
--------------------------------------------------------------------------------
1 | import type { Params } from 'react-router-dom'
2 |
3 | export interface IRouteInfo {
4 | params: Params
5 | query: { [key: string]: string | number } | undefined
6 | path: string | undefined
7 | fullPath: string | undefined
8 | id: string | undefined
9 | handle: {
10 | reProtect?: () => void
11 | }
12 | }
13 |
14 | export interface INavigateInfo {
15 | to: IRouteInfo | undefined
16 | from: IRouteInfo | undefined
17 | }
18 |
19 | export interface IRouteInit {
20 | id: string
21 | pathname: string
22 | params: Params
23 | data: unknown
24 | handle: unknown
25 | }
26 |
27 | const INIT_NAVIGATE_INFO: INavigateInfo = {
28 | to: undefined,
29 | from: undefined,
30 | }
31 |
32 | const INIT_ROUTE_INFO: IRouteInfo = {
33 | params: {},
34 | query: undefined,
35 | path: undefined,
36 | fullPath: undefined,
37 | id: undefined,
38 | handle: {},
39 | }
40 |
41 | const INIT_ROUTE_INIT: IRouteInit = {
42 | id: '',
43 | pathname: '',
44 | params: {},
45 | data: undefined,
46 | handle: undefined,
47 | }
48 |
49 | export const NavigateInfoContext =
50 | createContext(INIT_NAVIGATE_INFO)
51 |
52 | export function useNavigateInfo() {
53 | const navigateInfo = useContext(NavigateInfoContext)
54 | return navigateInfo
55 | }
56 |
57 | export const RouteInfoContext = createContext(INIT_ROUTE_INFO)
58 |
59 | export function useRoute() {
60 | const routeInfo = useContext(RouteInfoContext)
61 | return routeInfo
62 | }
63 |
64 | export function useParamsAdvance() {
65 | const routeInfo = useContext(RouteInfoContext)
66 |
67 | return routeInfo.params
68 | }
69 |
70 | export function useSearchQueryAdvance() {
71 | const routeInfo = useContext(RouteInfoContext)
72 |
73 | return routeInfo.query
74 | }
75 |
76 | export const RouteInitContext = createContext(
77 | INIT_ROUTE_INIT
78 | )
79 |
80 | export function useRouteInit() {
81 | const routeInit = useContext(RouteInitContext)
82 | return routeInit
83 | }
84 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "dev": "cross-env ENV=development PORT=3000 IO_PORT=3030 webpack serve --mode=development",
4 | "build": "tsc && webpack --mode=production",
5 | "build:esm": "tsc && cross-env ESM=true webpack --mode=production",
6 | "preview": "cross-env PORT=8080 NODE_NO_WARNINGS=1 node ./config/webpack.serve.config.js",
7 | "prettier": "tsc --noEmit && prettier src/**/*.{t,j}s{,x} --no-error-on-unmatched-pattern --check",
8 | "lint": "tsc --noEmit && eslint src/**/*.{t,j}s{,x} --no-error-on-unmatched-pattern --ignore-pattern node_modules/",
9 | "lint:fix": "npm run lint -- --fix",
10 | "prettier:fix": "npm run prettier -- --write",
11 | "format": "npm run prettier:fix && npm run lint:fix",
12 | "prepare": "git config core.autocrlf false && npx husky install",
13 | "pre-commit": "tsc && lint-staged"
14 | },
15 | "browserslist": [
16 | "> 1%",
17 | "last 2 versions",
18 | "not dead"
19 | ],
20 | "dependencies": {
21 | "polished": "^4.2.2",
22 | "react": "^18.2.0",
23 | "react-dom": "^18.2.0",
24 | "react-router-dom": "^6.14.1",
25 | "styled-components": "^6.0.2"
26 | },
27 | "devDependencies": {
28 | "commit-packages": "file:./config/commit-packages",
29 | "lint-and-format-packages": "file:./config/lint-and-format-packages",
30 | "build-tool-packages": "file:./config/build-tool-packages"
31 | },
32 | "eslintConfig": {
33 | "extends": [
34 | "./config/eslint.config.js"
35 | ]
36 | },
37 | "prettier": "./config/prettier.config.js",
38 | "lint-staged": {
39 | "*.{js,jsx,ts,tsx}": "npm run prettier:fix && npm run lint"
40 | },
41 | "commitlint": {
42 | "extends": [
43 | "@commitlint/config-conventional"
44 | ],
45 | "rules": {
46 | "type-enum": [
47 | 2,
48 | "always",
49 | [
50 | "feat",
51 | "style",
52 | "test",
53 | "fix",
54 | "docs",
55 | "release",
56 | "update",
57 | "perf",
58 | "chore"
59 | ]
60 | ]
61 | }
62 | },
63 | "config": {
64 | "commitizen": {
65 | "path": "cz-git",
66 | "czConfig": "./config/cz.config.js"
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/router/utils/RouterDeliver.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | INavigateInfo,
3 | IRouteInfo,
4 | NavigateInfoContext,
5 | RouteInfoContext,
6 | useRouteInit,
7 | } from 'app/router/context/InfoContext'
8 | import type { Params } from 'react-router'
9 |
10 | let navigateInfo: INavigateInfo = {
11 | from: undefined,
12 | to: undefined,
13 | }
14 |
15 | function useSplitParams(): Params {
16 | const matches = useMatches()
17 | const params = useParams()
18 |
19 | let newParams = {
20 | ...params,
21 | }
22 |
23 | matches.forEach(function (item) {
24 | const splitParams = (
25 | item as {
26 | handle?: {
27 | params?: {
28 | split: (params: Params) => Params
29 | }
30 | }
31 | }
32 | )?.handle?.params?.split
33 |
34 | if (params && typeof splitParams === 'function') {
35 | newParams = {
36 | ...newParams,
37 | ...(splitParams(params) || {}),
38 | }
39 | }
40 | })
41 |
42 | return newParams
43 | } // useSplitParams()
44 |
45 | export default function RouterDeliver({ children }) {
46 | const location = useLocation()
47 | const routeInit = useRouteInit()
48 | const params = useSplitParams()
49 | const queryString = location.search?.substring(1)
50 | const query = queryString
51 | ? JSON.parse(
52 | '{"' + queryString.replace(/&/g, '","').replace(/=/g, '":"') + '"}',
53 | function (key, value) {
54 | return key === '' ? value : decodeURIComponent(value)
55 | }
56 | )
57 | : undefined
58 |
59 | const routeInfo: IRouteInfo = {
60 | params,
61 | query,
62 | path: location.pathname,
63 | fullPath: location.pathname + location.search,
64 | id: routeInit?.id,
65 | handle: {},
66 | }
67 |
68 | navigateInfo = {
69 | from:
70 | navigateInfo.to && navigateInfo.to.fullPath !== routeInfo.fullPath
71 | ? navigateInfo.to
72 | : navigateInfo.from,
73 | to: routeInfo,
74 | }
75 |
76 | return (
77 |
78 |
79 | {children}
80 |
81 |
82 | )
83 | } // RouterDiliver()
84 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/bitcoin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
8 |
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 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/chili.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
6 |
7 |
11 |
14 |
17 |
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 |
--------------------------------------------------------------------------------
/src/assets/static/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/config/.eslintrc-auto-import.json:
--------------------------------------------------------------------------------
1 | {
2 | "globals": {
3 | "BrowserRouter": true,
4 | "ComponentType": true,
5 | "Dispatch": true,
6 | "HTMLAttributes": true,
7 | "HTMLProps": true,
8 | "Link": true,
9 | "NavLink": true,
10 | "Navigate": true,
11 | "Outlet": true,
12 | "React": true,
13 | "ReactNode": true,
14 | "Route": true,
15 | "RouterProvider": true,
16 | "Routes": true,
17 | "SetStateAction": true,
18 | "StrictMode": true,
19 | "Suspense": true,
20 | "componentDidCatch": true,
21 | "createBrowserRouter": true,
22 | "createContext": true,
23 | "createGlobalStyle": true,
24 | "createRef": true,
25 | "createRoot": true,
26 | "decode": true,
27 | "deleteCookie": true,
28 | "encode": true,
29 | "forwardRef": true,
30 | "generatePath": true,
31 | "generateSentenceCase": true,
32 | "generateTitleCase": true,
33 | "getCookie": true,
34 | "getCustomSlug": true,
35 | "getLocale": true,
36 | "getSlug": true,
37 | "getSlugWithoutDash": true,
38 | "getUnsignedLetters": true,
39 | "hashCode": true,
40 | "keyframes": true,
41 | "lazy": true,
42 | "memo": true,
43 | "rgba": true,
44 | "setCookie": true,
45 | "startTransition": true,
46 | "styled": true,
47 | "useCallback": true,
48 | "useContext": true,
49 | "useDebugValue": true,
50 | "useDeferredValue": true,
51 | "useEffect": true,
52 | "useHref": true,
53 | "useId": true,
54 | "useImperativeHandle": true,
55 | "useInRouterContext": true,
56 | "useInsertionEffect": true,
57 | "useLayoutEffect": true,
58 | "useLinkClickHandler": true,
59 | "useLocation": true,
60 | "useMatches": true,
61 | "useMemo": true,
62 | "useNavigate": true,
63 | "useNavigationType": true,
64 | "useOutlet": true,
65 | "useOutletContext": true,
66 | "useParams": true,
67 | "useReducer": true,
68 | "useRef": true,
69 | "useResolvedPath": true,
70 | "useRoute": true,
71 | "useRoutes": true,
72 | "useSearchParams": true,
73 | "useSentenceCase": true,
74 | "useSlug": true,
75 | "useSlugWithoutDash": true,
76 | "useState": true,
77 | "useSyncExternalStore": true,
78 | "useTitleCase": true,
79 | "useTransition": true,
80 | "useUnsignedLetters": true
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/comment-page/CommentRow.tsx:
--------------------------------------------------------------------------------
1 | import ImageItem, { Outer as ImageOuter } from 'components/ImageItem'
2 | import { Suspender } from 'utils/Suspender'
3 |
4 | const Row = styled.div`
5 | display: flex;
6 | margin-bottom: 24px;
7 |
8 | &:last-child {
9 | margin-bottom: 0;
10 | }
11 | `
12 | const AvatarCol = styled.div`
13 | margin-right: 8px;
14 | flex: 0 0 50px;
15 | height: 50px;
16 | ${ImageOuter} {
17 | height: 100%;
18 | width: 100%;
19 | overflow: hidden;
20 | border-radius: 50%;
21 | background-color: ${rgba(import.meta.env.STYLE_COLOR_DARK, 0.1)};
22 | background-size: 16px 16px;
23 | }
24 | `
25 | const MetaCol = styled.div`
26 | min-width: 0;
27 | flex: 1 1 auto;
28 | `
29 | const NameLabel = styled.p`
30 | margin-bottom: 8px;
31 | `
32 | const ContentLabel = styled.div``
33 |
34 | const suspender = Suspender()
35 |
36 | export default function CommentRow({ total }: { total?: number }) {
37 | const route = useRoute()
38 | const amount = total ? total : Math.floor(Math.random() * 4) + 1
39 |
40 | suspender.start(() => {
41 | const duration = 1500
42 | new Promise((res) => {
43 | setTimeout(function () {
44 | res('OK')
45 | }, duration)
46 | })
47 | .then((result) => {
48 | console.log(result)
49 | suspender.resolve(result)
50 | })
51 | .catch((err) => suspender.reject(err))
52 | })
53 |
54 | suspender.get()
55 |
56 | const commentItemList = new Array(amount).fill(null).map((val, idx) => (
57 |
58 |
59 |
60 |
61 |
62 |
63 | Proident consectetur deserunt officia consectetur ad aliqua do
64 | excepteur sit.
65 |
66 |
67 | Excepteur reprehenderit minim officia anim occaecat nostrud nulla
68 | elit. Excepteur officia fugiat nisi anim enim quis proident
69 | consectetur exercitation. Consequat eu ea enim ullamco. Amet elit ad
70 | sit ipsum magna consequat exercitation consectetur ullamco.
71 |
72 |
73 |
74 | ))
75 |
76 | if (route?.id !== import.meta.env.ROUTER_COMMENT_ID) {
77 | commentItemList.push(
78 |
82 | See more
83 |
84 | )
85 | }
86 |
87 | return <>{commentItemList}>
88 | }
89 |
--------------------------------------------------------------------------------
/config/utils/PortHandler/index.js:
--------------------------------------------------------------------------------
1 | const net = require('net')
2 | const fs = require('fs')
3 | const path = require('path')
4 | const ObjectToEnvConverter = require('../ObjectToEnvConverter')
5 |
6 | const envPortPath = path.resolve(__dirname, './.env')
7 |
8 | const readFileENVSync = () => {
9 | if (!fs.existsSync(envPortPath)) {
10 | return
11 | }
12 |
13 | const portInfoStringity = fs.readFileSync(envPortPath, {
14 | encoding: 'utf8',
15 | flag: 'r',
16 | })
17 |
18 | if (!portInfoStringity) return
19 |
20 | let portInfo = {}
21 | portInfoStringity.split('\n').forEach((line) => {
22 | const [name, value] = line.split('=')
23 | if (name && value) {
24 | portInfo[name] = value
25 | }
26 | })
27 |
28 | return portInfo
29 | } // readFileENVSync
30 |
31 | const writeFileENVSync = (port, name) => {
32 | if (!port || !name) return
33 |
34 | const portInfo = readFileENVSync() || {}
35 |
36 | portInfo[name] = port
37 |
38 | new Promise(function (resolve) {
39 | try {
40 | fs.writeFileSync(envPortPath, ObjectToEnvConverter(portInfo))
41 |
42 | resolve('done')
43 | } catch {}
44 | })
45 | } // writeFileENVSync
46 |
47 | const checkPort = (port) => {
48 | return new Promise((resolve) => {
49 | const server = net.createServer()
50 | server.unref()
51 | server.on('error', () => {
52 | resolve(false)
53 | })
54 | server.listen(port, () => {
55 | server.close(() => {
56 | resolve(true)
57 | })
58 | })
59 | })
60 | } // checkPort
61 |
62 | const findFreePort = async (port) => {
63 | let tmpPort = port
64 | while (true) {
65 | const isFree = await checkPort(tmpPort)
66 | if (isFree) {
67 | return tmpPort
68 | }
69 | tmpPort++
70 | }
71 | } // findFreePort
72 |
73 | const getPort = (name) => {
74 | const portInfo = readFileENVSync()
75 |
76 | if (!portInfo || (name && !portInfo[name])) return
77 |
78 | return name ? portInfo[name] : portInfo
79 | } // getPort
80 |
81 | const releasePort = (port) => {
82 | return new Promise((resolve, reject) => {
83 | const server = net.createServer()
84 | server.unref()
85 | server.on('error', (err) => {
86 | reject(err)
87 | })
88 | server.listen(port, () => {
89 | server.close(() => {
90 | resolve()
91 | })
92 | })
93 | })
94 | }
95 |
96 | module.exports = {
97 | findFreePort,
98 | releasePort,
99 | setPort: (() => {
100 | return writeFileENVSync
101 | })(),
102 | getPort,
103 | }
104 |
--------------------------------------------------------------------------------
/config/env/env.mjs:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { fileURLToPath } from 'url'
3 | import fs from 'fs'
4 |
5 | import ENV_DEFINE_LIST from '../../env/env-register.mjs'
6 | import ObjToEnvConverter from '../utils/ObjectToEnvConverter.js'
7 |
8 | const __filename = fileURLToPath(import.meta.url)
9 | const __dirname = path.dirname(__filename)
10 | const PROJECT_PATH = __dirname.replace(/\\/g, '/')
11 |
12 | const ENV_OBJECT_DEFAULT = {
13 | PORT: Number(),
14 | IO_PORT: Number(),
15 | LOCAL_ADDRESS: String(),
16 | LOCAL_HOST: String(),
17 | IPV4_ADDRESS: String(),
18 | IPV4_HOST: String(),
19 | IO_HOST: String(),
20 | }
21 | const ENV_OBJ_WITH_JSON_STRINGIFY_VALUE = { ...ENV_OBJECT_DEFAULT }
22 |
23 | const generateObjectFormatted = (obj, prefix) => {
24 | if (!obj || typeof obj !== 'object') return {}
25 | for (const key in obj) {
26 | let tmpKey = `${
27 | prefix ? prefix.toUpperCase() + '_' : ''
28 | }${key.toUpperCase()}`
29 |
30 | if (typeof obj[key] === 'object' && !obj[key].length) {
31 | for (const childKey in obj[key]) {
32 | setValueForObject(
33 | ENV_OBJECT_DEFAULT,
34 | tmpKey + '_' + childKey.toUpperCase(),
35 | obj[key][childKey]
36 | )
37 | setValueForObject(
38 | ENV_OBJ_WITH_JSON_STRINGIFY_VALUE,
39 | tmpKey + '_' + childKey.toUpperCase(),
40 | JSON.stringify(obj[key][childKey])
41 | )
42 | }
43 |
44 | delete obj[key]
45 | } else {
46 | setValueForObject(ENV_OBJECT_DEFAULT, tmpKey, obj[key])
47 | setValueForObject(
48 | ENV_OBJ_WITH_JSON_STRINGIFY_VALUE,
49 | tmpKey,
50 | JSON.stringify(obj[key])
51 | )
52 | delete obj[key]
53 | }
54 | }
55 | } // getObjectWithPrefix()
56 |
57 | const setValueForObject = (obj, key, value) => {
58 | if (!typeof obj === 'object' || !key) return
59 | obj[key] = value
60 | } // setValueForObject()
61 |
62 | // NOTE - First step is generate object formatted
63 | if (ENV_DEFINE_LIST.length) {
64 | ENV_DEFINE_LIST.forEach(function (item) {
65 | generateObjectFormatted(item.data, item.prefix)
66 | })
67 | }
68 | // End Region
69 |
70 | const promiseENVWriteFileSync = new Promise(function (resolve) {
71 | try {
72 | fs.writeFileSync(
73 | `${PROJECT_PATH}/.env`,
74 | ObjToEnvConverter(ENV_OBJECT_DEFAULT)
75 | )
76 | fs.writeFileSync(
77 | `${PROJECT_PATH}/env.json`,
78 | JSON.stringify(ENV_OBJ_WITH_JSON_STRINGIFY_VALUE)
79 | )
80 |
81 | resolve('done')
82 | } catch {}
83 | })
84 |
85 | export {
86 | ENV_OBJECT_DEFAULT,
87 | ENV_OBJ_WITH_JSON_STRINGIFY_VALUE,
88 | promiseENVWriteFileSync,
89 | }
90 |
--------------------------------------------------------------------------------
/src/assets/styles/layout/_reset.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | div,
4 | span,
5 | applet,
6 | object,
7 | iframe,
8 | h1,
9 | h2,
10 | h3,
11 | h4,
12 | h5,
13 | h6,
14 | p,
15 | blockquote,
16 | pre,
17 | a,
18 | abbr,
19 | acronym,
20 | address,
21 | big,
22 | cite,
23 | code,
24 | del,
25 | dfn,
26 | em,
27 | img,
28 | ins,
29 | kbd,
30 | q,
31 | s,
32 | samp,
33 | small,
34 | strike,
35 | strong,
36 | sub,
37 | sup,
38 | tt,
39 | var,
40 | b,
41 | u,
42 | i,
43 | center,
44 | dl,
45 | dt,
46 | dd,
47 | ol,
48 | ul,
49 | li,
50 | fieldset,
51 | form,
52 | label,
53 | legend,
54 | table,
55 | caption,
56 | tbody,
57 | tfoot,
58 | thead,
59 | tr,
60 | th,
61 | td,
62 | article,
63 | aside,
64 | canvas,
65 | details,
66 | embed,
67 | figure,
68 | figcaption,
69 | footer,
70 | header,
71 | hgroup,
72 | menu,
73 | nav,
74 | output,
75 | ruby,
76 | section,
77 | summary,
78 | time,
79 | mark,
80 | audio,
81 | video {
82 | margin: 0;
83 | padding: 0;
84 | border: 0;
85 | font-size: 100%;
86 | font: inherit;
87 | vertical-align: baseline;
88 | }
89 |
90 | * {
91 | box-sizing: border-box;
92 | line-height: 1.5 !important;
93 | }
94 |
95 | html {
96 | font-size: 8px;
97 | height: 100%;
98 |
99 | &.is-disable-scroll--g2j-basic-modal {
100 | &,
101 | & body {
102 | overflow: hidden;
103 | }
104 | }
105 | /* .is-disable-scroll--g2j-basic-modal */
106 | }
107 |
108 | body {
109 | position: relative;
110 | font-size: 14px;
111 | width: 100%;
112 | min-height: 100%;
113 | overflow-x: hidden;
114 | /* line-height: 1.5 !important; */
115 | background: #f2f2f2;
116 | color: $dark-color;
117 | margin: 0;
118 | }
119 |
120 | a {
121 | text-decoration: none;
122 | color: $dark-color;
123 | }
124 |
125 | /* HTML5 display-role reset for older browsers */
126 | article,
127 | aside,
128 | details,
129 | figcaption,
130 | figure,
131 | footer,
132 | header,
133 | hgroup,
134 | menu,
135 | nav,
136 | section {
137 | display: block;
138 | }
139 | ol,
140 | ul {
141 | list-style: none;
142 | }
143 | blockquote,
144 | q {
145 | quotes: none;
146 | }
147 | blockquote:before,
148 | blockquote:after,
149 | q:before,
150 | q:after {
151 | content: '';
152 | content: none;
153 | }
154 | table {
155 | border-collapse: collapse;
156 | border-spacing: 0;
157 | }
158 |
159 | img {
160 | max-width: 100%;
161 | max-height: 100%;
162 | }
163 |
164 | input,
165 | textarea {
166 | font-size: 2rem;
167 | }
168 |
169 | input,
170 | textarea,
171 | button,
172 | select,
173 | label,
174 | a {
175 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
176 | outline: none;
177 | }
178 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/sycee-gold.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
9 |
13 |
16 |
19 |
20 |
23 |
24 |
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 |
--------------------------------------------------------------------------------
/config/utils/WebpackCustomizeDefinePlugin.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack')
2 |
3 | class RuntimeUpdateValue {
4 | _pluginFn = null
5 | _options = null
6 |
7 | constructor(pluginFn, options) {
8 | this._pluginFn = pluginFn
9 | this._options = options
10 | }
11 |
12 | async getValue() {
13 | let tmpPluginValue =
14 | typeof this._pluginFn === 'function' ? this._pluginFn() : this._pluginFn
15 | tmpPluginValue =
16 | (await tmpPluginValue?.then?.(function (data) {
17 | return data
18 | })) ?? tmpPluginValue
19 |
20 | return typeof tmpPluginValue === 'object' &&
21 | tmpPluginValue instanceof Object &&
22 | !Array.isArray(tmpPluginValue)
23 | ? { ...tmpPluginValue, date: Date.now() }
24 | : tmpPluginValue
25 | }
26 |
27 | setRuntime(pluginKey, instanceOfPlugin, compiler) {
28 | if (!pluginKey || !instanceOfPlugin || !this._options || !compiler) return
29 |
30 | const self = this
31 |
32 | compiler.hooks.watchRun.tapAsync('MyPlugin', async (compilation, cb) => {
33 | if (
34 | compilation.modifiedFiles &&
35 | [...compilation.modifiedFiles][0] === this._options.fileDependencies
36 | ) {
37 | instanceOfPlugin.definitions[pluginKey] = await self.getValue()
38 | }
39 | cb()
40 | })
41 |
42 | compiler.hooks.afterCompile.tap('MyPlugin', (compilation) => {
43 | compilation.fileDependencies.add(this._options.fileDependencies)
44 | })
45 | }
46 | }
47 |
48 | class WebpackCustomizeDefinePlugin {
49 | _objPlugin = null
50 |
51 | constructor(objPlugin) {
52 | this._objPlugin = objPlugin
53 | }
54 |
55 | async apply(compiler) {
56 | if (
57 | !compiler ||
58 | !this._objPlugin ||
59 | !(
60 | typeof this._objPlugin === 'object' &&
61 | this._objPlugin instanceof Object &&
62 | !Array.isArray(this._objPlugin)
63 | )
64 | )
65 | return
66 |
67 | const objInstanceOfPlugin = {}
68 |
69 | // NOTE - The key is name of plugin that we want to define in project
70 | for (const key in this._objPlugin) {
71 | if (this._objPlugin[key] instanceof RuntimeUpdateValue) {
72 | objInstanceOfPlugin[key] = new webpack.DefinePlugin({
73 | [key]: await this._objPlugin[key].getValue(),
74 | })
75 | objInstanceOfPlugin[key].apply(compiler)
76 | this._objPlugin[key].setRuntime(key, objInstanceOfPlugin[key], compiler)
77 | } else if (
78 | typeof this._objPlugin[key] === 'string' ||
79 | (typeof this._objPlugin[key] === 'object' &&
80 | this._objPlugin[key] instanceof Object)
81 | ) {
82 | objInstanceOfPlugin[key] = new webpack.DefinePlugin({
83 | [key]: this._objPlugin[key],
84 | })
85 | objInstanceOfPlugin[key].apply(compiler)
86 | continue
87 | }
88 | }
89 | }
90 |
91 | static RuntimeUpdateValue(pluginFn, options) {
92 | return new RuntimeUpdateValue(pluginFn, options)
93 | }
94 | }
95 |
96 | module.exports = { WebpackCustomizeDefinePlugin }
97 |
--------------------------------------------------------------------------------
/src/pages/ContentPage.tsx:
--------------------------------------------------------------------------------
1 | import ModuleContentSection from 'components/content-page/ModuleContentSection'
2 | import CommentSection from 'components/comment-page/CommentSection'
3 | import { generatePath } from 'react-router-dom'
4 |
5 | // NOTE - Dummy Data Region
6 | const response: {
7 | [key: string | number]: {
8 | title: string
9 | content: string
10 | }
11 | } = {
12 | 1: {
13 | title: 'Excepteur nostrud deserunt do ipsum eu dolore.',
14 | content:
15 | 'Laboris cillum ex elit reprehenderit ad ullamco ut magna cupidatat labore veniam consectetur anim. Amet laborum ullamco velit nostrud sunt officia officia sunt consequat pariatur qui. Commodo nisi eiusmod aliquip officia pariatur esse labore tempor. Do ipsum nostrud nostrud commodo esse nisi reprehenderit tempor nostrud commodo voluptate fugiat sunt.',
16 | },
17 | 2: {
18 | title:
19 | 'Ex Lorem commodo nisi et qui adipisicing consectetur magna duis enim pariatur eu.',
20 | content:
21 | 'Nisi laborum sint culpa ea quis eu aliquip pariatur incididunt ad do sunt. Eu dolore mollit et Lorem velit deserunt qui ut. Consequat magna do esse nisi non cillum laboris dolor excepteur Lorem proident. Adipisicing esse ut eiusmod Lorem dolor enim.',
22 | },
23 | 3: {
24 | title: 'Dolor in voluptate anim magna.',
25 | content:
26 | 'Cupidatat consectetur occaecat magna excepteur aute est eiusmod. Lorem minim reprehenderit magna aliquip tempor pariatur tempor cupidatat irure esse ipsum eiusmod elit excepteur. Non minim dolore eu ullamco ea in occaecat fugiat. Consequat sunt mollit magna id occaecat commodo qui dolor id.',
27 | },
28 | 4: {
29 | title:
30 | 'Eiusmod exercitation sint adipisicing magna sit dolore adipisicing.',
31 | content:
32 | 'Aliqua sunt cupidatat ea ad est dolore. Ad reprehenderit eu labore adipisicing incididunt sit voluptate officia consequat minim proident. Sit minim laborum proident consequat fugiat pariatur ea ut exercitation. Ea aliqua do anim aute elit irure anim do. Ipsum aliqua sit et eu. Proident eu esse labore exercitation do nulla. Fugiat exercitation laborum id cupidatat qui.',
33 | },
34 | }
35 | // NOTE - End Dummy Data Region
36 |
37 | // NOTE - Styled Components Region
38 | const Page = styled.div``
39 | // NOTE - End Styled Components Region
40 |
41 | export default function ContentPage() {
42 | // const { slugs, id } = useParamsAdvance()
43 | const route = useRoute()
44 | console.log('rerender ContentPage')
45 |
46 | const data: {
47 | title: string
48 | content: string
49 | } = response[route.params.id as string]
50 |
51 | return (
52 |
53 |
54 |
55 | {'< Back to HomePage'}
56 |
57 |
58 | {route?.id !== import.meta.env.ROUTER_COMMENT_ID ? (
59 | <>
60 |
61 |
62 |
63 |
64 |
65 | {`> View Comment`}
66 |
67 |
68 |
69 |
70 |
71 | >
72 | ) : (
73 |
74 | )}
75 |
76 | )
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/router/utils/RouterProtection.tsx:
--------------------------------------------------------------------------------
1 | import type { INavigateInfo } from 'app/router/context/InfoContext'
2 | import { useNavigateInfo } from 'app/router/context/InfoContext'
3 | import type { IValidation } from 'app/router/context/ValidationContext'
4 | import type { To } from 'react-router'
5 | import useCertificateCustomizationInfo, {
6 | type ICertCustomizationInfo,
7 | } from '../hooks/useCertificateCustomizationInfo'
8 | import { IRouterProtectionProps, RouteObjectCustomize } from '../types'
9 |
10 | export interface ICertInfo extends ICertCustomizationInfo {
11 | navigateInfo: INavigateInfo
12 | successPath: string
13 | }
14 |
15 | let successPath: string
16 | let successID: string
17 | let WAITING_VERIFY_ROUTER_ID_LIST: { [key: string]: Array }
18 |
19 | function useProtectRoute(): IValidation {
20 | const navigate = useNavigate()
21 | const route = useRoute()
22 | const location = useLocation()
23 | const matches = useMatches()
24 | const protection: IValidation = {
25 | status: 200,
26 | }
27 |
28 | const certificateCustomizationInfo = useCertificateCustomizationInfo()
29 |
30 | const navigateInfo = useNavigateInfo()
31 |
32 | const certificateInfo: ICertInfo = {
33 | ...certificateCustomizationInfo,
34 | navigateInfo,
35 | successPath,
36 | }
37 |
38 | matches.some(function (item) {
39 | const protect = (
40 | item?.handle as
41 | | {
42 | protect: RouteObjectCustomize['handle']['protect']
43 | }
44 | | undefined
45 | )?.protect
46 |
47 | if (protect && typeof protect === 'function') {
48 | const checkProtection = (isReProtect = false) => {
49 | const protectInfo = protect(certificateInfo, route)
50 |
51 | if (!protectInfo) {
52 | protection.status = 301
53 | protection.redirect = -1
54 | } else if (typeof protectInfo === 'string') {
55 | if (WAITING_VERIFY_ROUTER_ID_LIST[route?.id ?? '']) {
56 | successPath = location.pathname + location.search
57 | successID = route?.id ?? ''
58 | }
59 |
60 | protection.status = 301
61 | protection.redirect = protectInfo
62 | }
63 |
64 | if (isReProtect && protection.status !== 200) {
65 | navigate(protection.redirect as To, {
66 | replace: protection.status === 301,
67 | })
68 | }
69 | }
70 |
71 | route.handle.reProtect = () => checkProtection(true)
72 |
73 | checkProtection()
74 | }
75 | })
76 |
77 | return protection
78 | } // useProtectRoute()
79 |
80 | export default function RouterProtection({
81 | waitingVerifyRouterIDList = {},
82 | children,
83 | }: IRouterProtectionProps) {
84 | WAITING_VERIFY_ROUTER_ID_LIST = waitingVerifyRouterIDList
85 |
86 | const route = useRoute()
87 | const protection = useProtectRoute()
88 |
89 | if (protection.status !== 200) {
90 | const to = protection.redirect || -1
91 |
92 | return
93 | }
94 |
95 | if (
96 | successID &&
97 | !WAITING_VERIFY_ROUTER_ID_LIST[successID].includes(route.id as string)
98 | ) {
99 | successID = ''
100 | successPath = ''
101 | }
102 |
103 | return children
104 | } // RouterProtect()
105 |
--------------------------------------------------------------------------------
/src/app/router/index.tsx:
--------------------------------------------------------------------------------
1 | import Layout from 'Layout'
2 | import NotFoundPage from 'pages/NotFoundPage'
3 | import { RouteObject } from 'react-router-dom'
4 | import { withLazy } from './utils/LazyComponentHandler'
5 | import RouterDeliver from './utils/RouterDeliver'
6 | import RouterInit from './utils/RouterInit'
7 | import RouterProtection from './utils/RouterProtection'
8 | import RouterValidation from './utils/RouterValidation'
9 | import { LoadingInfoProvider } from './context/LoadingInfoContext'
10 | import { RouteObjectCustomize } from './types'
11 |
12 | // NOTE - Router Configuration
13 | const routes: RouteObjectCustomize[] = [
14 | {
15 | path: import.meta.env.ROUTER_BASE_PATH,
16 | element: (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | ),
29 | children: [
30 | {
31 | index: true,
32 | path: import.meta.env.ROUTER_HOME_PATH,
33 | element: withLazy(() => import('pages/HomePage')),
34 | }, // Home Page
35 | {
36 | path: import.meta.env.ROUTER_CONTENT_PATH,
37 | element: withLazy(() => import('pages/ContentPage')),
38 | handle: {
39 | params: {
40 | validate(p) {
41 | if (typeof p.slugs === 'string') {
42 | return /\d+$/.test(p.slugs as string)
43 | }
44 |
45 | return true
46 | },
47 | split(p) {
48 | return {
49 | slug: p.slugs?.match(/^[a-zA-Z-_.]+[a-zA-Z]/)?.[0],
50 | id: p.slugs?.match(/\d+$/)?.[0],
51 | }
52 | },
53 | },
54 | },
55 | children: [
56 | {
57 | path: import.meta.env.ROUTER_CONTENT_COMMENT_PATH,
58 | element: withLazy(
59 | () => import('components/comment-page/CommentRow')
60 | ),
61 | },
62 | {
63 | id: import.meta.env.ROUTER_COMMENT_ID,
64 | path: import.meta.env.ROUTER_COMMENT_PATH,
65 | element: withLazy(() => import('pages/CommentPage')),
66 |
67 | handle: {
68 | protect(certInfo) {
69 | const userInfo = certInfo?.user
70 |
71 | if (!userInfo || !userInfo.email)
72 | return import.meta.env.ROUTER_LOGIN_PATH
73 |
74 | return true
75 | },
76 | },
77 | },
78 | ],
79 | }, // Content Page
80 | {
81 | id: import.meta.env.ROUTER_LOGIN_ID,
82 | path: import.meta.env.ROUTER_LOGIN_PATH,
83 | element: withLazy(() => import('pages/LoginPage')),
84 | handle: {
85 | protect(certInfo) {
86 | const userInfo = certInfo?.user
87 |
88 | if (userInfo && userInfo.email) {
89 | return certInfo.successPath
90 | ? certInfo.successPath
91 | : certInfo.navigateInfo?.from
92 | ? certInfo.navigateInfo.from.fullPath
93 | : import.meta.env.ROUTER_HOME_PATH
94 | }
95 |
96 | return true
97 | },
98 | },
99 | }, // Login Page
100 | {
101 | path: import.meta.env.ROUTER_NOT_FOUND_PATH,
102 | element: ,
103 | },
104 | ],
105 | },
106 | ]
107 |
108 | const router = createBrowserRouter(routes as RouteObject[], {
109 | basename: '/',
110 | })
111 |
112 | export default router
113 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "exclude": [
3 | "node_modules",
4 | "dist",
5 | "package.json",
6 | "package-lock.json",
7 | "yarn-lock.json"
8 | ],
9 | "files": [
10 | "config/auto-imports.d.ts",
11 | "config/types/ImportMeta.d.ts",
12 | "src/static.d.ts"
13 | ],
14 | "include": ["src/**/*.js", "src/**/*.ts", "src/**/*.jsx", "src/**/*.tsx"],
15 | "compilerOptions": {
16 | "useDefineForClassFields": true,
17 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
18 | // NOTE - Set what module will be built for project (es, commonjs) (what is compile way will be chosen for import / export syntax)
19 | // https://www.typescriptlang.org/tsconfig#module
20 | // https://stackoverflow.com/questions/41993811/understanding-target-and-module-in-tsconfig
21 | "module": "ESNext",
22 |
23 | // NOTE - Set what target ECMAScript target version (you can easy to think that the target option is used to setup for what compile way for script syntax that exclude import and export)
24 | // https://www.typescriptlang.org/tsconfig#target
25 | // https://stackoverflow.com/questions/39493003/typescript-compile-options-module-vs-target
26 | "target": "ESNext",
27 |
28 | // NOTE - Set way to system try to find module
29 | // https://www.typescriptlang.org/tsconfig#moduleResolution
30 | // https://www.typescriptlang.org/tsconfig#moduleResolution
31 | // https://www.codementor.io/@elliotaplant/understanding-javascript-module-resolution-systems-with-dinosaurs-il2oqro6e
32 | "moduleResolution": "Node",
33 |
34 | // NOTE - Base path of project setup with this tsconfig
35 | "baseUrl": "src",
36 |
37 | // NOTE - Setup resolve alias path for project (use case is for auto complete sugestion)
38 | "paths": {
39 | "@/*": ["../*"],
40 | "assets/*": ["assets/*"],
41 | "app/*": ["app/*"],
42 | "hooks/*": ["hooks/*"],
43 | "pages/*": ["pages/*"],
44 | "components/*": ["components/*"],
45 | "store/*": ["store/*"],
46 | "utils/*": ["utils/*"]
47 | },
48 |
49 | // NOTE - Tell typescript compile just only check type
50 | // https://www.typescriptlang.org/tsconfig#noEmit
51 | // https://stackoverflow.com/questions/55002137/typescript-noemit-use-case
52 | "noEmit": true,
53 |
54 | // NOTE - This option allow us to use .tsx in project
55 | // https://www.typescriptlang.org/tsconfig#jsx
56 | // https://stackoverflow.com/questions/62859458/what-is-the-use-of-jsx-property-in-tsconfig-json
57 | // "jsx": "preserve",
58 | // "jsx": "preserve",
59 | "jsx": "react",
60 |
61 | // NOTE - Check for auto import
62 | // "checkJs": true,
63 |
64 | // NOTE - Allow JavaScript files to be imported inside your project
65 | // https://www.typescriptlang.org/tsconfig#allowJs
66 | "allowJs": true,
67 |
68 | // NOTE - Setup strict mode for project (eslint also use this config to figure out error)
69 | // https://www.typescriptlang.org/tsconfig#strict
70 | // https://dev.to/jsdev/strict-mode-typescript-j8p
71 | "strict": false,
72 | // "noImplicitAny": false,
73 |
74 | // NOTE - This option will skip type checking of declaration files if enable
75 | // https://www.typescriptlang.org/tsconfig#skipLibCheck
76 | "skipLibCheck": true,
77 | "forceConsistentCasingInFileNames": true,
78 | "resolveJsonModule": true,
79 |
80 | // NOTE - With flag esModuleInterop we can import CommonJS modules in compliance with es6 modules spec
81 | // https://stackoverflow.com/questions/56238356/understanding-esmoduleinterop-in-tsconfig-file
82 | "esModuleInterop": true,
83 |
84 | // NOTE - Allow you write (import React from "react";) instead of (import * as React from "react";)
85 | // https://www.typescriptlang.org/tsconfig#allowSyntheticDefaultImports
86 | "allowSyntheticDefaultImports": true,
87 | "importHelpers": true,
88 | "sourceMap": true,
89 | "isolatedModules": true
90 | // "types": ["webpack-env", "react"]
91 | },
92 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"]
93 | }
94 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/cannabis.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
7 |
8 |
10 |
12 |
13 |
14 |
16 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
3 | // prefix: 'gj-',
4 | theme: {
5 | screens: {
6 | 375: '375px',
7 | 390: '390px',
8 | 425: '425px',
9 | 480: '480px',
10 | 640: '640px',
11 | 740: '740px',
12 | 768: '768px',
13 | 992: '992px',
14 | 1024: '1024px',
15 | 1200: '1200px',
16 | 1366: '1366px',
17 | }, // screens
18 |
19 | colors: {
20 | primary: {
21 | 300: '#B34600',
22 | 200: '#D95500',
23 | 70: '#FF8332',
24 | 40: '#FFC198',
25 | 10: '#FFEFE5',
26 | DEFAULT: '#FF6400',
27 | },
28 | white: '#FFFFFF',
29 | borderElement: '#D1D1D6',
30 | gray: {
31 | 80: '#1C1C1E',
32 | 60: '#3A3A3C',
33 | 40: '#636366',
34 | 30: '#AEAEB2',
35 | 20: '#D1D1D6',
36 | 10: '#F2F2F7',
37 | },
38 | yellow: {
39 | 50: '#FFE680',
40 | 10: '#FFF0B3',
41 | DEFAULT: '#FFD326',
42 | },
43 | green: {
44 | 50: '#30DA5B',
45 | 10: '#CEFDD5',
46 | DEFAULT: '#20B845',
47 | },
48 | red: {
49 | 50: '#FF6961',
50 | 10: '#FFAAA6',
51 | DEFAULT: '#D70015',
52 | },
53 | purple: {
54 | 50: '#DA8EFF',
55 | 10: '#E6CDFD',
56 | DEFAULT: '#8943AB',
57 | },
58 | blue: '#3872DD',
59 | }, // colors
60 |
61 | fontSize: {
62 | 10: ['10px', { lineHeight: '1.5' }],
63 | 12: ['12px', { lineHeight: '1.5' }],
64 | 14: ['14px', { lineHeight: '1.5' }],
65 | 16: ['16px', { lineHeight: '1.5' }],
66 | 18: ['18px', { lineHeight: '29px' }],
67 | 20: ['20px', { lineHeight: '1.5' }],
68 | 22: ['22px', { lineHeight: '36px' }],
69 | 24: ['24px', { lineHeight: '1.5' }],
70 | 28: ['28px', { lineHeight: '45px' }],
71 | 32: ['32px', { lineHeight: '52px' }],
72 | 34: ['34px', { lineHeight: '1.5' }],
73 | 36: ['36px', { lineHeight: '1.5' }],
74 | 40: ['40px', { lineHeight: '1.5' }],
75 | 42: ['42px', { lineHeight: '1.5' }],
76 | 48: ['48px', { lineHeight: '1.5' }],
77 | }, // fontSize
78 |
79 | borderRadius: {
80 | 4: '4px',
81 | 8: '8px',
82 | 10: '10px',
83 | 12: '12px',
84 | 16: '16px',
85 | 20: '20px',
86 | 27: '27px',
87 | round: '50%',
88 | }, // borderRadius
89 |
90 | boxShadow: {
91 | '02030': '0px 20px 30px rgba(0, 0, 0, 0.1)',
92 | 2420: '2px 4px 20px rgba(0, 0, 0, 0.2)',
93 | }, // boxShadow
94 |
95 | spacing: {
96 | 0: '0px',
97 | 2: '2px',
98 | 4: '4px',
99 | 6: '6px',
100 | 8: '8px',
101 | 10: '10px',
102 | 12: '12px',
103 | 14: '14px',
104 | 16: '16px',
105 | 18: '18px',
106 | 20: '20px',
107 | 22: '22px',
108 | 24: '24px',
109 | 26: '26px',
110 | 28: '28px',
111 | 30: '30px',
112 | 32: '32px',
113 | 34: '34px',
114 | 36: '36px',
115 | 38: '38px',
116 | 40: '40px',
117 | 42: '42px',
118 | 44: '44px',
119 | 46: '46px',
120 | 48: '48px',
121 | 50: '50px',
122 | 52: '52px',
123 | 54: '54px',
124 | 56: '56px',
125 | 58: '58px',
126 | 60: '60px',
127 | 62: '62px',
128 | 64: '64px',
129 | 66: '66px',
130 | 68: '68px',
131 | 70: '70px',
132 | 72: '72px',
133 | 74: '74px',
134 | 76: '76px',
135 | 78: '78px',
136 | 80: '80px',
137 | }, // spacing
138 | fontWeight: {
139 | regular: 400,
140 | semibold: 600,
141 | bold: 700,
142 | normal: 'normal',
143 | },
144 | fontFamily: {
145 | 'nunito-sans': ['Nunito Sans', 'Roboto', 'Helvetica Neue', 'sans-serif'],
146 |
147 | 'svn-poppins': ['SVN-Poppins', 'Roboto', 'Helvetica Neue', 'sans-serif'],
148 |
149 | fas: 'Fontawesome 6 Pro',
150 | }, // fontFamily
151 |
152 | fontStyle: {
153 | normal: 'normal',
154 | italic: 'italic',
155 | }, // fontStyle
156 |
157 | extend: {
158 | lineHeight: {
159 | 29: '29px',
160 | 36: '36px',
161 | 45: '45px',
162 | 52: '52px',
163 | },
164 | margin: {
165 | 8: '8px',
166 | 16: '16px',
167 | },
168 | flexBasis: {
169 | 45: '45%',
170 | },
171 | },
172 | },
173 | plugins: [],
174 | }
175 |
--------------------------------------------------------------------------------
/config/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | /* prettier-ignore */
3 | // @ts-nocheck
4 | // noinspection JSUnusedGlobalSymbols
5 | // Generated by unplugin-auto-import
6 | export {}
7 | declare global {
8 | const BrowserRouter: typeof import('react-router-dom')['BrowserRouter']
9 | const Link: typeof import('react-router-dom')['Link']
10 | const NavLink: typeof import('react-router-dom')['NavLink']
11 | const Navigate: typeof import('react-router-dom')['Navigate']
12 | const Outlet: typeof import('react-router-dom')['Outlet']
13 | const React: typeof import('react')
14 | const Route: typeof import('react-router-dom')['Route']
15 | const RouterProvider: typeof import('react-router-dom')['RouterProvider']
16 | const Routes: typeof import('react-router-dom')['Routes']
17 | const StrictMode: typeof import('react')['StrictMode']
18 | const Suspense: typeof import('react')['Suspense']
19 | const componentDidCatch: typeof import('react')['componentDidCatch']
20 | const createBrowserRouter: typeof import('react-router-dom')['createBrowserRouter']
21 | const createContext: typeof import('react')['createContext']
22 | const createGlobalStyle: typeof import('styled-components')['createGlobalStyle']
23 | const createRef: typeof import('react')['createRef']
24 | const createRoot: typeof import('react-dom/client')['createRoot']
25 | const decode: typeof import('utils/StringHelper.ts')['decode']
26 | const deleteCookie: typeof import('utils/CookieHelper.ts')['deleteCookie']
27 | const encode: typeof import('utils/StringHelper.ts')['encode']
28 | const forwardRef: typeof import('react')['forwardRef']
29 | const generatePath: typeof import('react-router-dom')['generatePath']
30 | const generateSentenceCase: typeof import('utils/StringHelper.ts')['generateSentenceCase']
31 | const generateTitleCase: typeof import('utils/StringHelper.ts')['generateTitleCase']
32 | const getCookie: typeof import('utils/CookieHelper.ts')['getCookie']
33 | const getCustomSlug: typeof import('utils/StringHelper.ts')['getCustomSlug']
34 | const getLocale: typeof import('utils/StringHelper.ts')['getLocale']
35 | const getSlug: typeof import('utils/StringHelper.ts')['getSlug']
36 | const getSlugWithoutDash: typeof import('utils/StringHelper.ts')['getSlugWithoutDash']
37 | const getUnsignedLetters: typeof import('utils/StringHelper.ts')['getUnsignedLetters']
38 | const hashCode: typeof import('utils/StringHelper.ts')['hashCode']
39 | const keyframes: typeof import('styled-components')['keyframes']
40 | const lazy: typeof import('react')['lazy']
41 | const memo: typeof import('react')['memo']
42 | const rgba: typeof import('polished')['rgba']
43 | const setCookie: typeof import('utils/CookieHelper.ts')['setCookie']
44 | const startTransition: typeof import('react')['startTransition']
45 | const styled: typeof import('styled-components')['default']
46 | const useCallback: typeof import('react')['useCallback']
47 | const useContext: typeof import('react')['useContext']
48 | const useDebugValue: typeof import('react')['useDebugValue']
49 | const useDeferredValue: typeof import('react')['useDeferredValue']
50 | const useEffect: typeof import('react')['useEffect']
51 | const useHref: typeof import('react-router-dom')['useHref']
52 | const useId: typeof import('react')['useId']
53 | const useImperativeHandle: typeof import('react')['useImperativeHandle']
54 | const useInRouterContext: typeof import('react-router-dom')['useInRouterContext']
55 | const useInsertionEffect: typeof import('react')['useInsertionEffect']
56 | const useLayoutEffect: typeof import('react')['useLayoutEffect']
57 | const useLinkClickHandler: typeof import('react-router-dom')['useLinkClickHandler']
58 | const useLocation: typeof import('react-router-dom')['useLocation']
59 | const useMatches: typeof import('react-router-dom')['useMatches']
60 | const useMemo: typeof import('react')['useMemo']
61 | const useNavigate: typeof import('react-router-dom')['useNavigate']
62 | const useNavigationType: typeof import('react-router-dom')['useNavigationType']
63 | const useOutlet: typeof import('react-router-dom')['useOutlet']
64 | const useOutletContext: typeof import('react-router-dom')['useOutletContext']
65 | const useParams: typeof import('react-router-dom')['useParams']
66 | const useReducer: typeof import('react')['useReducer']
67 | const useRef: typeof import('react')['useRef']
68 | const useResolvedPath: typeof import('react-router-dom')['useResolvedPath']
69 | const useRoute: typeof import('app/router/context/InfoContext')['useRoute']
70 | const useRoutes: typeof import('react-router-dom')['useRoutes']
71 | const useSearchParams: typeof import('react-router-dom')['useSearchParams']
72 | const useSentenceCase: typeof import('hooks/useStringHelper.ts')['useSentenceCase']
73 | const useSlug: typeof import('hooks/useStringHelper.ts')['useSlug']
74 | const useSlugWithoutDash: typeof import('hooks/useStringHelper.ts')['useSlugWithoutDash']
75 | const useState: typeof import('react')['useState']
76 | const useSyncExternalStore: typeof import('react')['useSyncExternalStore']
77 | const useTitleCase: typeof import('hooks/useStringHelper.ts')['useTitleCase']
78 | const useTransition: typeof import('react')['useTransition']
79 | const useUnsignedLetters: typeof import('hooks/useStringHelper.ts')['useUnsignedLetters']
80 | }
81 | // for type re-export
82 | declare global {
83 | // @ts-ignore
84 | export type { Dispatch, SetStateAction, HTMLProps, HTMLAttributes, ComponentType, ReactNode } from 'react'
85 | import('react')
86 | }
87 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/onion-green.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
10 |
13 |
16 |
17 |
23 |
27 |
31 |
34 |
35 |
38 |
41 |
45 |
48 |
51 |
54 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/config/webpack.production.config.js:
--------------------------------------------------------------------------------
1 | const { DefinePlugin } = require('webpack')
2 | const glob = require('glob')
3 | const TerserPlugin = require('terser-webpack-plugin')
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
6 | const { PurgeCSSPlugin } = require('purgecss-webpack-plugin')
7 |
8 | module.exports = (async () => {
9 | const { ENV_OBJ_WITH_JSON_STRINGIFY_VALUE } = await import('./env/env.mjs')
10 |
11 | return {
12 | mode: 'production',
13 | output: {
14 | publicPath: '/',
15 | ...(process.env.ESM
16 | ? {
17 | module: true,
18 | environment: {
19 | dynamicImport: true,
20 | },
21 | }
22 | : {}),
23 | },
24 | ...(process.env.ESM
25 | ? {
26 | // externalsType: 'module',
27 | externals: {
28 | react: 'module https://esm.sh/react@18.2.0',
29 | 'react-dom': 'module https://esm.sh/react-dom@18.2.0',
30 | 'react-router-dom': 'module https://esm.sh/react-router-dom@6.6.2',
31 | 'styled-components':
32 | 'module https://esm.sh/styled-components@5.3.6',
33 | polished: 'module https://esm.sh/polished@4.2.2',
34 | },
35 | }
36 | : {}),
37 | module: {
38 | rules: [
39 | {
40 | test: /\.(js|jsx|ts|tsx)$/,
41 | exclude: /node_modules|dist/,
42 | use: {
43 | loader: 'babel-loader',
44 | options: {
45 | presets: [
46 | [
47 | '@babel/preset-env',
48 | {
49 | bugfixes: true,
50 | useBuiltIns: 'entry',
51 | corejs: 3,
52 | },
53 | ],
54 | '@babel/preset-react',
55 | '@babel/preset-typescript',
56 | ],
57 | plugins: [
58 | ['@babel/plugin-transform-class-properties', { loose: false }],
59 | ],
60 | },
61 | },
62 | },
63 | ],
64 | },
65 | plugins: [
66 | new PurgeCSSPlugin({
67 | paths: ['./index.production.html'].concat(
68 | glob.sync(`./src/**/*`, { nodir: true })
69 | ),
70 | }),
71 | new HtmlWebpackPlugin({
72 | title: 'webpack project for react',
73 | template: 'index.production.html',
74 | inject: 'body',
75 | templateParameters: {
76 | env: process.env.ENV,
77 | ioHost: JSON.stringify(process.env.IO_HOST),
78 | },
79 | scriptLoading: process.env.ESM ? 'module' : 'defer',
80 | minify: {
81 | collapseWhitespace: true,
82 | removeComments: true,
83 | removeRedundantAttributes: true,
84 | removeScriptTypeAttributes: true,
85 | removeStyleLinkTypeAttributes: true,
86 | useShortDoctype: true,
87 | },
88 | }),
89 | new DefinePlugin({
90 | 'import.meta.env': ENV_OBJ_WITH_JSON_STRINGIFY_VALUE,
91 | }),
92 | ],
93 | stats: {
94 | assetsSort: '!size',
95 | children: false,
96 | usedExports: false,
97 | modules: false,
98 | entrypoints: false,
99 | excludeAssets: [/\.*\.map/],
100 | },
101 | cache: {
102 | type: 'filesystem',
103 | allowCollectingMemory: true,
104 | memoryCacheUnaffected: true,
105 | compression: 'gzip',
106 | },
107 | performance: {
108 | maxEntrypointSize: 512000,
109 | maxAssetSize: 512000,
110 | hints: false,
111 | },
112 | optimization: {
113 | moduleIds: 'deterministic',
114 | runtimeChunk: 'single',
115 | splitChunks: {
116 | chunks: 'all',
117 | minSize: 5000,
118 | maxSize: 100000,
119 |
120 | cacheGroups: {
121 | styles: {
122 | type: 'css/mini-extract',
123 | filename: '[contenthash:8].css',
124 | priority: 100,
125 | minSize: 1000,
126 | maxSize: 50000,
127 | minSizeReduction: 50000,
128 | enforce: true,
129 | },
130 | vendor: {
131 | chunks: 'all',
132 | test: /[\\/]node_modules[\\/]/,
133 | filename: '[chunkhash:8].js',
134 | enforce: true,
135 | reuseExistingChunk: true,
136 | },
137 | utils: {
138 | chunks: 'all',
139 | test: /[\\/]utils[\\/]/,
140 | filename: '[chunkhash:8].js',
141 | reuseExistingChunk: true,
142 | minSize: 10000,
143 | maxSize: 100000,
144 | },
145 | app: {
146 | chunks: 'all',
147 | test: /[\\/]app[\\/]/,
148 | filename: '[chunkhash:8].js',
149 | reuseExistingChunk: true,
150 | minSize: 10000,
151 | maxSize: 100000,
152 | },
153 | store: {
154 | chunks: 'all',
155 | test: /[\\/]store[\\/]/,
156 | filename: '[chunkhash:8].js',
157 | reuseExistingChunk: true,
158 | minSize: 10000,
159 | maxSize: 100000,
160 | },
161 | hooks: {
162 | chunks: 'all',
163 | test: /[\\/]hooks[\\/]/,
164 | filename: '[chunkhash:8].js',
165 | reuseExistingChunk: true,
166 | minSize: 10000,
167 | maxSize: 100000,
168 | },
169 | },
170 | },
171 |
172 | minimize: true,
173 | minimizer: [
174 | new TerserPlugin({
175 | parallel: 4,
176 | terserOptions: {
177 | format: {
178 | comments: false, // It will drop all the console.log statements from the final production build
179 | },
180 | compress: {
181 | // drop_console: true, // It will stop showing any console.log statement in dev tools. Make it false if you want to see consoles in production mode.
182 | },
183 | },
184 | extractComments: false,
185 | }),
186 | // new ESBuildMinifyPlugin({
187 | // target: 'es2015',
188 | // }),
189 | new CssMinimizerPlugin({
190 | exclude: /node_modules/,
191 | parallel: 4,
192 |
193 | minify: [
194 | // CssMinimizerPlugin.esbuildMinify,
195 | CssMinimizerPlugin.cssnanoMinify,
196 | CssMinimizerPlugin.cssoMinify,
197 | CssMinimizerPlugin.cleanCssMinify,
198 | ],
199 | }),
200 | ],
201 | }, // optimization
202 | target: process.env.ESM ? 'web' : 'browserslist',
203 | ...(process.env.ESM
204 | ? {
205 | experiments: {
206 | outputModule: true,
207 | },
208 | }
209 | : {
210 | experiments: {
211 | cacheUnaffected: true,
212 | },
213 | }),
214 | }
215 | })()
216 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/money.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
9 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
40 |
42 |
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 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/bug.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
11 |
15 |
20 |
25 |
29 |
34 |
38 |
39 |
40 |
41 |
42 |
46 |
50 |
52 |
53 |
55 |
57 |
59 |
60 |
62 |
64 |
65 |
69 |
72 |
73 |
76 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/onion-purple.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
11 |
14 |
15 |
16 |
21 |
24 |
28 |
30 |
34 |
38 |
49 |
51 |
54 |
57 |
60 |
62 |
64 |
65 |
67 |
70 |
73 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const { CleanWebpackPlugin } = require('clean-webpack-plugin')
4 | const CopyPlugin = require('copy-webpack-plugin')
5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
6 |
7 | const PROJECT_PATH = __dirname.replace(/\\/g, '/')
8 | const resolve =
9 | resolveTsconfigPathsToAlias(path.resolve(PROJECT_PATH, 'tsconfig.json')) || {}
10 |
11 | module.exports = async (env, arg) => {
12 | const WebpackConfigWithMode = await require(getWebpackConfigFilePathWithMode(
13 | arg.mode
14 | ))
15 |
16 | if (!WebpackConfigWithMode) return
17 |
18 | return {
19 | mode: WebpackConfigWithMode.mode || arg.mode || 'production',
20 | context: path.resolve(__dirname, '.'),
21 | entry: {
22 | app: {
23 | import: '/src/index.tsx',
24 | },
25 | ...(WebpackConfigWithMode.entry || {}),
26 | },
27 | output: {
28 | pathinfo: false,
29 | globalObject: 'window',
30 | filename: '[contenthash:8].js',
31 | path: path.resolve(__dirname, 'dist'),
32 | ...(WebpackConfigWithMode.output || {}),
33 | },
34 | externalsType: WebpackConfigWithMode.externalsType || 'global',
35 | externals: WebpackConfigWithMode.externals || {},
36 | resolve: {
37 | preferRelative: true,
38 | alias: {
39 | ...resolve.alias,
40 | ...(WebpackConfigWithMode.resolve?.alias ?? {}),
41 | },
42 | extensions: ['.ts', '.js', '.tsx', '.jsx'],
43 | modules: [
44 | 'node_modules',
45 | path.resolve(__dirname, './node_modules'),
46 | ...resolve.modules,
47 | ],
48 | },
49 | devtool: WebpackConfigWithMode.devtool || false,
50 | devServer: WebpackConfigWithMode.devServer || {},
51 | module: {
52 | rules: [
53 | {
54 | test: /\.((c|sa|sc)ss)$/i,
55 | use: [
56 | {
57 | // NOTE - We should use option 1 because if we use 'style-loader' then the import .css will be replace by of sfc vue compiler
58 | // NOTE - Option 2
59 | // loader: 'style-loader',
60 |
61 | // NOTE - Option 1
62 | loader: MiniCssExtractPlugin.loader,
63 | },
64 | {
65 | loader: 'css-loader',
66 | options: { url: false },
67 | },
68 | {
69 | loader: 'postcss-loader',
70 | options: {
71 | postcssOptions: {
72 | plugins: [
73 | 'postcss-preset-env',
74 | 'postcss-simple-vars',
75 | 'tailwindcss/nesting',
76 | 'autoprefixer',
77 | require('tailwindcss')(
78 | PROJECT_PATH + '/tailwind.config.js'
79 | ),
80 | ],
81 | },
82 | },
83 | },
84 | ],
85 | },
86 | // NOTE - This config to resolve asset's paths
87 | {
88 | test: /\.(png|jpe?g|gif|webm|mp4|svg|ico|tff|eot|otf|woff|woff2)$/,
89 | type: 'asset/resource',
90 | generator: {
91 | emit: false,
92 | // filename: '[hash][ext][query]',
93 | },
94 | exclude: [/node_modules/],
95 | },
96 | ...(WebpackConfigWithMode?.module?.rules ?? []),
97 | ],
98 | unsafeCache: true,
99 | noParse: /[\\/]src\/assets\/static[\\/]|libs[\\/]socket.io.min.js/,
100 | },
101 | plugins: [
102 | new CleanWebpackPlugin(),
103 | new CopyPlugin({
104 | patterns: [
105 | {
106 | from: './src/assets/static',
107 | filter: (resourcePath) => {
108 | if (
109 | arg.mode === 'production' &&
110 | resourcePath.indexOf('images/development') !== -1
111 | ) {
112 | return false
113 | }
114 |
115 | return true
116 | },
117 | },
118 | ],
119 | }),
120 | new MiniCssExtractPlugin({
121 | filename:
122 | arg.mode === 'development' ? '[id].css' : '[id].[contenthash:8].css',
123 | chunkFilename:
124 | arg.mode === 'development' ? '[id].css' : '[id].[contenthash:8].css',
125 | ignoreOrder: false,
126 | experimentalUseImportModule: true,
127 | }),
128 | require('unplugin-auto-import/webpack')({
129 | // targets to transform
130 | include: [
131 | /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
132 | /\.md$/, // .md
133 | ],
134 | imports: [
135 | // presets
136 | 'react',
137 | {
138 | react: [
139 | ['*', 'React'],
140 | 'Suspense',
141 | 'componentDidCatch',
142 | 'StrictMode',
143 | 'createContext',
144 | ],
145 | },
146 | {
147 | from: 'react',
148 | imports: [
149 | 'Dispatch',
150 | 'SetStateAction',
151 | 'HTMLProps',
152 | 'HTMLAttributes',
153 | 'ComponentType',
154 | 'ReactNode',
155 | ],
156 | type: true,
157 | },
158 | {
159 | 'react-dom/client': ['createRoot'],
160 | },
161 | 'react-router-dom',
162 | {
163 | 'react-router-dom': [
164 | 'createBrowserRouter',
165 | 'RouterProvider',
166 | 'BrowserRouter',
167 | 'useMatches',
168 | 'generatePath',
169 | ],
170 | },
171 | {
172 | 'app/router/context/InfoContext': ['useRoute'],
173 | 'utils/StringHelper.ts': [
174 | 'getSlug',
175 | 'getSlugWithoutDash',
176 | 'getUnsignedLetters',
177 | 'getCustomSlug',
178 | 'generateTitleCase',
179 | 'generateSentenceCase',
180 | 'getLocale',
181 | 'encode',
182 | 'decode',
183 | 'hashCode',
184 | ],
185 | 'hooks/useStringHelper.ts': [
186 | 'useSlug',
187 | 'useSlugWithoutDash',
188 | 'useUnsignedLetters',
189 | 'useTitleCase',
190 | 'useSentenceCase',
191 | ],
192 | 'utils/CookieHelper.ts': ['getCookie', 'setCookie', 'deleteCookie'],
193 | },
194 | {
195 | 'styled-components': [
196 | ['default', 'styled'],
197 | 'createGlobalStyle',
198 | 'keyframes',
199 | ],
200 | },
201 | {
202 | polished: ['rgba'],
203 | },
204 | ],
205 | dts: PROJECT_PATH + '/config/auto-imports.d.ts',
206 | eslintrc: {
207 | enabled: true,
208 | filepath: PROJECT_PATH + '/config/.eslintrc-auto-import.json',
209 | },
210 | }),
211 | ...(WebpackConfigWithMode.plugins || []),
212 | ],
213 | stats: WebpackConfigWithMode.stats || 'detailed',
214 | cache: WebpackConfigWithMode.cache || true,
215 | performance: WebpackConfigWithMode.performance || {},
216 | optimization: WebpackConfigWithMode.optimization || {},
217 | experiments: WebpackConfigWithMode.experiments || {},
218 | target: WebpackConfigWithMode.target || 'web',
219 | node: WebpackConfigWithMode.node || {},
220 | }
221 | }
222 |
223 | /**
224 | * Libs inline support for webpack config
225 | */
226 | const getWebpackConfigFilePathWithMode = (mode) => {
227 | if (!mode) return
228 |
229 | return mode === 'development'
230 | ? './config/webpack.development.config.js'
231 | : './config/webpack.production.config'
232 | } // getWebpackConfigFilePathWithMode(mode?: 'development' | 'production')
233 |
234 | function resolveTsconfigPathsToAlias(tsconfigPath = './tsconfig.json') {
235 | // const tsconfig = require(tsconfigPath)
236 | // const { paths, baseUrl } = tsconfig.compilerOptions
237 | // NOTE - Get json content without comment line (ignore error JSON parse some string have unexpected symbol)
238 | // https://stackoverflow.com/questions/40685262/read-json-file-ignoring-custom-comments
239 | const tsconfig = JSON.parse(
240 | fs
241 | .readFileSync(tsconfigPath)
242 | ?.toString()
243 | .replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) =>
244 | g ? '' : m
245 | )
246 | )
247 | const { paths, baseUrl } = tsconfig.compilerOptions
248 |
249 | const modules = [path.resolve(__dirname, baseUrl)]
250 |
251 | const alias = Object.fromEntries(
252 | Object.entries(paths)
253 | .filter(([, pathValues]) => pathValues.length > 0)
254 | .map(([pathKey, pathValues]) => {
255 | const key = pathKey.replace('/*', '')
256 | const value = path.resolve(
257 | __dirname,
258 | baseUrl,
259 | pathValues[0].replace(/[\/|\*]+(?:$)/g, '')
260 | )
261 | modules.push(value)
262 | return [key, value]
263 | })
264 | )
265 |
266 | return {
267 | alias: {
268 | src: path.resolve(__dirname, baseUrl),
269 | ...alias,
270 | },
271 | modules,
272 | }
273 | } // resolveTsconfigPathsToAlias()
274 |
--------------------------------------------------------------------------------
/src/assets/static/images/development/svg/medicines.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
8 |
10 |
13 |
16 |
18 |
20 |
23 |
25 |
27 |
29 |
31 |
33 |
34 |
36 |
37 |
38 |
39 |
40 |
47 |
58 |
69 |
79 |
84 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
--------------------------------------------------------------------------------
/config/webpack.development.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 | const webpack = require('webpack')
4 | const HtmlWebpackPlugin = require('html-webpack-plugin')
5 | const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin')
6 | const {
7 | WebpackCustomizeDefinePlugin,
8 | } = require('./utils/WebpackCustomizeDefinePlugin.js')
9 | const { IPV4_ADDRESS } = require('./utils/NetworkGenerator.js')
10 | const { getPort, findFreePort } = require('./utils/PortHandler/index.js')
11 |
12 | // NOTE - Setup process.env for address and host
13 | if (process.env) {
14 | process.env.LOCAL_ADDRESS = 'localhost'
15 | process.env.IPV4_ADDRESS = IPV4_ADDRESS || process.env.LOCAL_ADDRESS
16 | process.env.LOCAL_HOST = `localhost:${process.env.PORT || 3000}`
17 | process.env.IPV4_HOST = `${process.env.IPV4_ADDRESS}:${
18 | process.env.PORT || 3000
19 | }`
20 | process.env.IO_HOST = `${process.env.IPV4_ADDRESS}:${
21 | process.env.IO_PORT || 3030
22 | }`
23 | }
24 | // End setup process.env for address and host
25 |
26 | const SocketInitial = require('./utils/SocketHandler.js')
27 | let _socket = null
28 |
29 | const WebpackDevelopmentConfiguration = async () => {
30 | const PROJECT_PATH = __dirname.replace(/\\/g, '/')
31 | const WEBPACK_DEV_SERVER_PORT = await findFreePort(3000)
32 | const SOCKET_IO_PORT = getPort('SOCKET_IO_PORT')
33 |
34 | // NOTE - Setup process.env for address and host
35 | if (process.env) {
36 | process.env.LOCAL_ADDRESS = 'localhost'
37 | process.env.IPV4_ADDRESS = IPV4_ADDRESS || process.env.LOCAL_ADDRESS
38 | process.env.LOCAL_HOST = `localhost:${WEBPACK_DEV_SERVER_PORT}`
39 | process.env.IPV4_HOST = `${process.env.IPV4_ADDRESS}:${WEBPACK_DEV_SERVER_PORT}`
40 | process.env.IO_HOST = `${process.env.IPV4_ADDRESS}:${SOCKET_IO_PORT}`
41 | }
42 | // end setup
43 |
44 | const port = WEBPACK_DEV_SERVER_PORT
45 |
46 | await initENVHandler()
47 |
48 | return {
49 | mode: 'development',
50 | port,
51 | entry: {},
52 | output: {
53 | publicPath: '/',
54 | environment: {
55 | dynamicImport: true,
56 | },
57 | // module: true,
58 | // library: { type: 'module' },
59 | },
60 | // devtool: 'inline-source-map', // NOTE - BAD Performance, GOOD debugging
61 | devtool: 'eval-source-map', // NOTE - BAD Performance, GOOD debugging
62 | // devtool: 'eval-cheap-module-source-map', // NOTE - SLOW Performance, GOOD debugging
63 | // devtool: 'eval', // NOTE - GOOD Performance, BAD debugging
64 | // devtool: 'eval-cheap-source-map',
65 | devServer: {
66 | compress: true,
67 | port,
68 | static: './dist',
69 | watchFiles: ['src/**/*', 'config/templates/index.*.html'],
70 | hot: true,
71 | liveReload: false,
72 | host: process.env.PROJECT_IPV4_HOST,
73 | client: {
74 | overlay: false,
75 | logging: 'warn', // Want to set this to 'warn' or 'error'
76 | }, // NOTE - Use overlay of react refresh plugin
77 | historyApiFallback: true,
78 | },
79 | module: {
80 | rules: [
81 | // NOTE - Option 2
82 | // {
83 | // test: /.(jsx|tsx|js|ts)$/,
84 | // exclude: /(node_modules)/,
85 | // use: {
86 | // loader: 'swc-loader',
87 | // options: {
88 | // jsc: {
89 | // parser: {
90 | // syntax: 'typescript',
91 | // tsx: true,
92 | // decorators: false,
93 | // dynamicImport: true,
94 | // },
95 | // target: 'esnext',
96 | // },
97 | // },
98 | // },
99 | // },
100 | // NOTE - Option 1 (popular)
101 | {
102 | test: /\.(jsx|tsx|js|ts)$/,
103 | use: {
104 | loader: 'esbuild-loader',
105 | options: {
106 | loader: 'tsx',
107 | target: 'esnext',
108 | },
109 | },
110 | exclude: /node_modules|dist/,
111 | },
112 | {
113 | test: /libs[\\/]socket.io.min.js/,
114 | type: 'asset/resource',
115 | generator: {
116 | filename: '[name][ext]',
117 | },
118 | },
119 | ],
120 | },
121 | plugins: [
122 | new HtmlWebpackPlugin({
123 | title: 'webpack project for vue',
124 | // template: './config/templates/index.development.pacman-loading.html',
125 | template: './config/templates/index.development.image-loading.html',
126 | inject: 'body',
127 | templateParameters: {
128 | env: process.env.ENV,
129 | ioHost: JSON.stringify(process.env.IO_HOST),
130 | },
131 | // scriptLoading: 'module',
132 | }),
133 | RecompileLoadingScreenInitial,
134 | new ReactRefreshPlugin(),
135 | // { esModule: true, overlay: true }
136 | new WebpackCustomizeDefinePlugin({
137 | 'import.meta.env': WebpackCustomizeDefinePlugin.RuntimeUpdateValue(
138 | () => {
139 | let objEnvDefault = null
140 |
141 | return new Promise((resolve) => {
142 | let result = null
143 | try {
144 | result = fs.readFileSync(`${PROJECT_PATH}/env/env.json`)
145 | result = result ? JSON.parse(result) : {}
146 | } catch (err) {
147 | console.log(
148 | '=============\nError Message:\nRead env.json file process is wrong!\nIf you need setup env, make sure you have run create-dts package script\n============='
149 | )
150 | }
151 |
152 | objEnvDefault = {
153 | PORT: JSON.stringify(process.env.PORT),
154 | IO_PORT: JSON.stringify(process.env.IO_PORT),
155 | LOCAL_ADDRESS: JSON.stringify(process.env.LOCAL_ADDRESS),
156 | LOCAL_HOST: JSON.stringify(process.env.LOCAL_HOST),
157 | IPV4_ADDRESS: JSON.stringify(process.env.IPV4_ADDRESS),
158 | IPV4_HOST: JSON.stringify(process.env.IPV4_HOST),
159 | IO_HOST: JSON.stringify(process.env.IO_HOST),
160 | }
161 |
162 | result = {
163 | ...result,
164 | ...objEnvDefault,
165 | }
166 |
167 | resolve(result)
168 | })
169 | },
170 | {
171 | fileDependencies: path.resolve(__dirname, './env/.env'),
172 | }
173 | ),
174 | }),
175 | new webpack.ProgressPlugin({
176 | // NOTE - https://webpack.js.org/plugins/progress-plugin/#usage
177 | percentBy: 'entries',
178 | handler: (() => {
179 | // NOTE - At the first time, system will compile 3 process instead 1
180 | let totalProcess = 3
181 | let tmpTotalPercentagePerTotalProcess = 0
182 | let tmpTotalPercentage = 0
183 | let totalPercentage = 0
184 |
185 | return function (percentage) {
186 | if (!_socket) {
187 | return
188 | }
189 |
190 | if (percentage === 0)
191 | tmpTotalPercentagePerTotalProcess = totalPercentage
192 |
193 | tmpTotalPercentage =
194 | tmpTotalPercentagePerTotalProcess < 100
195 | ? tmpTotalPercentagePerTotalProcess +
196 | Math.ceil(percentage * 100) / totalProcess
197 | : Math.ceil(percentage * 100)
198 |
199 | totalPercentage =
200 | tmpTotalPercentage > totalPercentage || tmpTotalPercentage === 0
201 | ? tmpTotalPercentage
202 | : totalPercentage + 0.5
203 |
204 | _socket.emit('updateProgressPercentage', totalPercentage)
205 | }
206 | })(),
207 | }),
208 | ].filter(Boolean),
209 |
210 | stats: {
211 | preset: 'errors-only',
212 | all: false,
213 | },
214 |
215 | cache: {
216 | // NOTE - Type memory
217 | type: 'memory',
218 | cacheUnaffected: true,
219 | },
220 |
221 | optimization: {
222 | runtimeChunk: false,
223 | removeAvailableModules: false,
224 | removeEmptyChunks: false,
225 | splitChunks: false,
226 | sideEffects: false,
227 | providedExports: false,
228 | },
229 | // NOTE - refer to: https://github.com/webpack/webpack/issues/12102
230 | experiments: {
231 | lazyCompilation: {
232 | imports: true,
233 | entries: true,
234 | test: (module) =>
235 | !/[\\/](node_modules|src\/(utils|config|assets))[\\/]/.test(
236 | module.nameForCondition()
237 | ),
238 | },
239 | layers: true,
240 | cacheUnaffected: true,
241 | },
242 | }
243 | }
244 |
245 | class RecompileLoadingScreen {
246 | _socketEmitTurnOnLoadingScreenTimeout = null
247 | _socketEmitTurnOffLoadingScreenTimeout = null
248 | _isFinishFirstCompiling = false
249 |
250 | constructor() {
251 | this._setupSocketConnection()
252 | }
253 |
254 | async _setupSocketConnection() {
255 | const self = this
256 | await SocketInitial.then(function (data) {
257 | _socket = data?.socket
258 | data?.setupCallback?.(self._setupSocketReconnection.bind(self))
259 | })
260 | }
261 |
262 | _setupSocketReconnection(data) {
263 | if (!data || !this._isFinishFirstCompiling) return
264 | _socket = data?.socket
265 | }
266 |
267 | _stopTimeoutTurnOnProcessing() {
268 | clearTimeout(this._socketEmitTurnOffLoadingScreenTimeout)
269 | this._socketEmitTurnOffLoadingScreenTimeout = null
270 | } // _stopTimeoutTurnOnProcessing()
271 |
272 | _setTimeoutTurnOnProcessingWithDuration(duration) {
273 | if (!duration) {
274 | _socket.emit('turnOnLoadingScreen')
275 | } else {
276 | const self = this
277 | self._socketEmitTurnOnLoadingScreenTimeout = setTimeout(function () {
278 | _socket.emit('turnOnLoadingScreen')
279 | clearTimeout(self._socketEmitTurnOnLoadingScreenTimeout)
280 | }, duration)
281 | }
282 | } // _setTimeoutTurnOnProcessingWithDuration()
283 |
284 | _stopTimeoutTurnOffProcessing() {
285 | clearTimeout(this._socketEmitTurnOffLoadingScreenTimeout)
286 | this._socketEmitTurnOffLoadingScreenTimeout = null
287 | } // _stopTimeoutTurnOffProcessing()
288 |
289 | _setTimeoutTurnOffProcessingWithDuration(duration) {
290 | if (!duration) {
291 | _socket.emit('turnOffLoadingScreen')
292 | } else {
293 | const self = this
294 | self._socketEmitTurnOffLoadingScreenTimeout = setTimeout(function () {
295 | _socket.emit('turnOffLoadingScreen')
296 | clearTimeout(self._socketEmitTurnOffLoadingScreenTimeout)
297 | }, duration)
298 | }
299 | } // _setTimeoutTurnOffProcessingWithDuration()
300 |
301 | apply(compiler) {
302 | const self = this
303 | compiler.hooks.watchRun.tap('RecompileLoadingScreen', () => {
304 | if (!self._isFinishFirstCompiling || !_socket) return
305 |
306 | if (self._socketEmitTurnOnLoadingScreenTimeout) {
307 | self._stopTimeoutTurnOnProcessing()
308 | }
309 |
310 | if (self._socketEmitTurnOffLoadingScreenTimeout) {
311 | self._stopTimeoutTurnOffProcessing()
312 | }
313 |
314 | self._setTimeoutTurnOnProcessingWithDuration()
315 | }) // compiler.hooks.watchRun
316 |
317 | compiler.hooks.done.tap('RecompileLoadingScreen', () => {
318 | if (!self._isFinishFirstCompiling || !_socket) {
319 | self._isFinishFirstCompiling = true
320 | return
321 | }
322 |
323 | if (self._socketEmitTurnOnLoadingScreenTimeout) {
324 | self._stopTimeoutTurnOnProcessing()
325 | }
326 |
327 | if (self._socketEmitTurnOffLoadingScreenTimeout) {
328 | self._stopTimeoutTurnOffProcessing()
329 | }
330 |
331 | self._setTimeoutTurnOffProcessingWithDuration(70)
332 | }) // compiler.hooks.done
333 | }
334 | }
335 |
336 | const RecompileLoadingScreenInitial = new RecompileLoadingScreen()
337 |
338 | const initENVHandler = async () => {
339 | await import('./types/dts-generator.mjs').then(async (data) => {
340 | if (!data) return
341 |
342 | return await data.promiseENVWriteFileSync.then(function () {
343 | const chokidar = require('chokidar')
344 | const { exec } = require('child_process')
345 |
346 | const envWatcher = chokidar.watch('./env/env*.mjs', {
347 | ignored: /$^/,
348 | persistent: true,
349 | }) // /$^/ is match nothing
350 |
351 | envWatcher.on('change', function () {
352 | exec('node ./config/types/dts-generator.mjs', () => {})
353 |
354 | RecompileLoadingScreenInitial._setTimeoutTurnOnProcessingWithDuration(
355 | 10
356 | )
357 | })
358 |
359 | const serverPuppeteerSSRWatcher = chokidar.watch(
360 | ['./server/utils/**/*.ts', './server/puppeteer-ssr/**/*.ts'],
361 | {
362 | ignored: /$^/,
363 | persistent: true,
364 | }
365 | ) // /$^/ is match nothing
366 |
367 | serverPuppeteerSSRWatcher.on('change', function () {
368 | RecompileLoadingScreenInitial._setTimeoutTurnOnProcessingWithDuration()
369 | let totalDuration = 1
370 | const interval = setInterval(() => {
371 | const percentage = Math.ceil((totalDuration * 100) / 8000)
372 | _socket?.emit(
373 | 'updateProgressPercentage',
374 | percentage > 30 ? percentage : 30 + percentage
375 | )
376 |
377 | if (totalDuration >= 8000) {
378 | clearInterval(interval)
379 | _socket?.emit('hardReload')
380 | return
381 | }
382 |
383 | totalDuration += 1
384 | })
385 | })
386 | })
387 | })
388 | } // initENVHandler()
389 |
390 | module.exports = WebpackDevelopmentConfiguration()
391 |
--------------------------------------------------------------------------------
/config/templates/index.development.image-loading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= htmlWebpackPlugin.options.title %>
9 |
10 | <% if(env==='development' ) { %>
11 |
12 | <% } %>
13 | <% if(env==='development') { %>
14 |
147 | <% } %>
148 |
149 |
150 |
151 |
152 | <% if(env==='development' ) { %>
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | <% } %>
179 | <% if(env==='development' ) { %>
180 |
341 | <% } %>
342 |
343 |
344 |
345 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## First start
2 |
3 | In this repository I will discuss about
4 |
5 | - How to define the basic router information in this project ?
6 | - How to use lazy loading in react-router and customize it ?
7 | - How to use Suspense to create loading page for loading process ?
8 | - How to validate a route ?
9 | - How to protect a route ?
10 |
11 | For more information about this project.
12 |
13 | 1. You can read detail about advanced structure of Webpack + React + TS project in this [repository](https://github.com/anhchangvt1994/webpack-project--template-react-ts).
14 | 2. You can read about react-router in [here](https://reactrouter.com/en/main).
15 |
16 | ## Table of contents
17 |
18 | 1. [Install](#install)
19 | 2. [Introduction](#introduction)
20 |
21 | Install
22 |
23 | ##### Expect Node 18.x or higher
24 |
25 | Clone source with SSH url:
26 |
27 | ```bash
28 | git clone https://github.com/anhchangvt1994/webpack-project--template-react-ts__react-router
29 | ```
30 |
31 | Install:
32 |
33 | ```bash
34 | cd webpack-project--template-react-ts__react-router
35 | ```
36 |
37 | If use npm
38 |
39 | ```bash
40 | npm install
41 | ```
42 |
43 | If use yarn 1.x
44 |
45 | ```bash
46 | yarn install
47 | ```
48 |
49 | Introduction
50 |
51 | ### Table of benefit information that you must know
52 |
53 | - [Define router information](#define)
54 | - [lazy-loading](#lazy-loading)
55 | - [Suspense](#Suspense)
56 | - [Validate on route](#validate)
57 | - [Protect on route](#protect)
58 |
59 | Define router information
60 | In this project, you can define router information in two ways
61 |
62 | 1. Immediacy
63 |
64 | This way will fast and easy to define and create a react-router. See code below.
65 |
66 | ```jsx
67 | // config/router/index.jsx
68 | import Layout from 'Layout.jsx'
69 | import HomePage from 'pages/HomePage.jsx'
70 |
71 | const routes = [
72 | {
73 | path: '/',
74 | element: ,
75 | children: [
76 | {
77 | index: true,
78 | path: '/',
79 | element: ,
80 | },
81 | ...
82 | ],
83 | },
84 | ]
85 |
86 | const router = createBrowserRouter(routes, {
87 | basename: '/',
88 | })
89 | ```
90 |
91 | 2. Define and use it by using environment variables
92 |
93 | This way will make you spend more time to define and create react-router-dom. But other way you can reuse it to compare route's name, makesure right result when create route's path by using ` ` and more. See code below
94 |
95 | ```javascript
96 | // env.router.mjs
97 | export default {
98 | prefix: 'router',
99 | data: {
100 | base: {
101 | path: '/',
102 | },
103 | home: {
104 | id: 'HomePage',
105 | path: '/',
106 | },
107 | home: {
108 | id: 'ContentPage',
109 | path: '/:slugs',
110 | },
111 | ...
112 | },
113 | }
114 | ```
115 |
116 | ```jsx
117 | // config/router/index.jsx
118 | import Layout from 'Layout.jsx'
119 | import HomePage from 'pages/HomePage.jsx'
120 |
121 | const routes = [
122 | {
123 | path: import.meta.env.ROUTER_BASE_PATH,
124 | element: ,
125 | children: [
126 | {
127 | index: true,
128 | path: import.meta.env.ROUTER_HOME_PATH,
129 | element: ,
130 | },
131 | ...
132 | ],
133 | },
134 | ]
135 |
136 | const router = createBrowserRouter(routes, {
137 | basename: '/',
138 | })
139 | ```
140 |
141 | ```jsx
142 | // HomePage.jsx
143 | const pathInfo = generatePath(import.meta.env.ROUTER_CONTENT_PATH, {
144 | slugs: 'a-b-c-d-1234',
145 | })
146 |
147 | return
148 | ```
149 |
150 | Imagine that what happend if you use the first solution to create react-router-dom information and change a path. Are you sure that you changed all of them ?
151 |
152 | lazy-loading
153 |
154 | In react, it already introduced a simple way to create a lazy-loading by using dynamic import with [lazy](https://reactjs.org/docs/code-splitting.html#reactlazy) method. See code below and more about [lazy-loading route](https://www.positronx.io/react-lazy-loading-router-using-react-router-dom-tutorial/)
155 |
156 | ```jsx
157 | // config/router/index.jsx
158 | import Layout from 'Layout.jsx'
159 |
160 | const HomePage = lazy(() => import('pages/HomePage.jsx'));
161 |
162 | const routes = [
163 | {
164 | path: import.meta.env.ROUTER_BASE_PATH,
165 | element: ,
166 | children: [
167 | {
168 | index: true,
169 | path: import.meta.env.ROUTER_HOME_PATH,
170 | element: ,
171 | },
172 | ...
173 | ],
174 | },
175 | ]
176 |
177 | const router = createBrowserRouter(routes, {
178 | basename: '/',
179 | })
180 | ```
181 |
182 | Suspense
183 |
184 | Suspense is a loading resolver solution. Imagine that your page is loading the HomePage resource (by using import dynamic on route) or await an API requesting, and your internet connection is so bad. Bumb! You see a blank page or a current page in so long of time, and that's the time you have to show a loading page or a skeleton.
185 |
186 | In this section, I will discuss about handling the loading page when using lazy-loading routes.
187 |
188 | Continue the problem above, we can resolve in two ways
189 |
190 | 1. Listen on Route and Listen on Hook
191 | Step by step like this
192 |
193 | - When a route init or change, the Layout will re-render. I will turn on load in this event.
194 | - When lazy-loading finish, the page component will active hooks. I will turn off loading screen at before logic code.
195 |
196 | route init/change > Layout re-render > turn on loading screen > lazy-loading route finish > lifecycle hooks of page actived > turn off loading screen.
197 |
198 | 2. Use Suspense
199 |
200 | In the first solution, we use router event and react hook + store to keep flag of on/off the loading screen. It means we have to :
201 |
202 | - Define a isLoading flag in store.
203 | - Define the turn on script in Layout.
204 | - Define the turn of script in each page's hook.
205 |
206 | I think that's run well, but not good for management.
207 |
208 | In the second solution, we will use Suspense to resolve loading screen with better management. See code below.
209 |
210 | ```jsx
211 | // config/router/index.jsx
212 | import Layout from 'Layout.jsx'
213 |
214 | const HomePage = lazy(() => import('pages/HomePage.jsx'));
215 |
216 | const routes = [
217 | {
218 | path: import.meta.env.ROUTER_BASE_PATH,
219 | element: ,
220 | children: [
221 | {
222 | index: true,
223 | path: import.meta.env.ROUTER_HOME_PATH,
224 | element: ,
225 | },
226 | ...
227 | ],
228 | },
229 | ]
230 |
231 | const router = createBrowserRouter(routes, {
232 | basename: '/',
233 | })
234 | ```
235 |
236 | ```jsx
237 | import LoadingBoundary from 'utils/LoadingBoundary'
238 | import LoadingPageComponent from 'components/LoadingPageComponent'
239 |
240 | function Layout() {
241 | const location = useLocation()
242 | return (
243 |
244 | }
248 | >
249 |
250 |
251 |
252 | )
253 | } // App()
254 |
255 | export default Layout
256 | ```
257 |
258 | Easy! And you're finish him. You created loading screen in just two files instead of multiples files like the first solution.
259 |
260 | NOTE: The ` ` tag is already add into ` ` used to custom delay time to show loading screen. If you need to know more about this customization, you can search on `LoadingBoundary`.
261 |
262 | Validate on route
263 |
264 | You can use regex to validate route in **react-router v5**, See code below
265 |
266 | ```javascript
267 | // /a-b-c-d-e1234 -> wrong
268 | // /a-b-c-d-1234 -> right
269 | {
270 | path: '/:slugs([a-zA-Z]-(\\d+$))'
271 | }
272 | ```
273 |
274 | That really cool feature! But this feature was [removed in react-router v6](https://reactrouter.com/en/main/start/faq#what-happened-to-regexp-routes-paths). So, you can't use regex to validate on route, instead that you can create a new hook for resolve the nesscessary of validation.
275 |
276 | In this project, I already create and integrate that hook for you. See code below, and readmore about inline comment in code.
277 |
278 | ```javascript
279 | {
280 | path: import.meta.env.ROUTER_CONTENT_PATH,
281 | element: withLazy(() => import('pages/ContentPage')),
282 | handle: {
283 | // It means validate and split method is params's method
284 | params: {
285 | // p is shorthand of params
286 | // If p.slugs is a string, we can validate it. Else, it is an undefined we can pass it.
287 | validate(p) {
288 | if (typeof p.slugs === 'string') {
289 | // /a-b-c-d-e1234 -> go to not found page
290 | // /a-b-c-d-1234 -> go to target page
291 | return /[a-zA-Z-_.]+[a-zA-Z]-(\d+$)/.test(p.slugs)
292 | }
293 |
294 | return true
295 | },
296 | // split method is a solution using to sperarate params to small and detail params.
297 | // not same vue-router, react-router doesn't have multiple params define in each slash
298 | // vue-router: /:title-:id -> right
299 | // react-router: /:title-:id -> wrong
300 | // so, to split slugs params to detail params information, we have to custom it by handle.split.
301 | split(p) {
302 | return {
303 | slug: p.slugs?.match(/^[a-zA-Z-_.]+[a-zA-Z]/)?.[0],
304 | id: p.slugs?.match(/\d+$/)?.[0],
305 | }
306 | },
307 | }
308 | },
309 | ```
310 |
311 | Tada! it's finish!
312 |
313 | As you can see, in react you must know more than and do more than, to create helpful utils and hooks for your project. But in this project the react-router looks like more simple.
314 |
315 | Protect on route
316 |
317 | You can protect route by using the [handle options and renderless component](https://reactrouter.com/en/main/hooks/use-matches).
318 | We will make an example in this project. The PRD for protect route's case has some short description.
319 |
320 | ```markdown
321 | // PRD - Comment Page
322 |
323 | **Description**
324 |
325 | - Comment Page is the page contains full list of comments.
326 | - The user can enter Comment Page with two ways:
327 |
328 | 1. Click "See more" from comment section in Content Page.
329 | 2. copy and paste the url of Comment Page in browser's url bar.
330 |
331 | **Accessible rules**
332 | To access Comment Page user must:
333 |
334 | - Already logged
335 |
336 | If user haven't logged before, the system will auto redirect user to Login Page.
337 |
338 | If user have an account before, user will logged with that account, and after login success, the system will redirect user back to Comment Page.
339 |
340 | If user does not have an account before, user can go to Register Page and regist an account. After regist success, the system will auto login and redirect user go to Comment Page.
341 | ```
342 |
343 | You will have many choice to resolve for above PRD
344 |
345 | 1. Use React Hook and Store (easy way to handle but never easy way to manage)
346 |
347 | - Router load Comment Page finish
348 | - Comment Page's hook actived
349 | - Check access rule. If invalid then store current path and redirect to Login Page
350 | - Login success redirect back to Comment Page path stored and remember clear that store's path variable.
351 |
352 | 2. Use only react-router (harder to implement but easy to use and manage)
353 |
354 | - Setup **handle { protect() {... return boolean | string} }** and execute it in **RouterProtection** render-less.
355 | - If **protect()** return invalid, then the system will auto check if Comment Page need to back after success verify, then save the path of Comment Page, and redirect user to Login Page.
356 | - Login success redirect back to Comment Page.
357 |
358 | In this project, I will show you the second solution. Cause we just focus only react-router in this project, and cause redirect is a part of router's cases, so doesn't need use store and hook to resolve it.
359 |
360 | I handled for you executing **protect()** in this project, so you just only focus how to use it easy way. See code below
361 |
362 | ```javascript
363 | // router/index
364 |
365 | // Init RouterProtection with WAITING_VERIFY_ROUTER_ID_LIST
366 | const WAITING_VERIFY_ROUTER_ID_LIST: { [key: string]: Array } = {
367 | [import.meta.env.ROUTER_COMMENT_ID]: [import.meta.env.ROUTER_LOGIN_ID],
368 | }
369 |
370 | {
371 | path: import.meta.env.ROUTER_BASE_PATH,
372 | element: (
373 |
374 | ...
375 |
378 |
379 |
380 | ...
381 |
382 | ),
383 | }
384 |
385 | // Config Protect method
386 | {
387 | id: import.meta.env.ROUTER_COMMENT_ID,
388 | path: import.meta.env.ROUTER_COMMENT_PATH,
389 | element: withLazy(() => import('pages/CommentPage')),
390 |
391 | handle: {
392 | protect(certInfo) {
393 | /**
394 | * certInfo param contains
395 | * {
396 | * user: {email?: string}
397 | * navigateInfo: {to: RouteLocationNormalized, from: RouteLocationNormalized}
398 | * successPath: string
399 | * }
400 | */
401 | const userInfo = certInfo?.user
402 |
403 | if (!userInfo || !userInfo.email)
404 | return import.meta.env.ROUTER_LOGIN_PATH
405 |
406 | return true
407 | },
408 | },
409 | },
410 | {
411 | id: import.meta.env.ROUTER_LOGIN_ID,
412 | path: import.meta.env.ROUTER_LOGIN_PATH,
413 | element: withLazy(() => import('pages/LoginPage')),
414 | handle: {
415 | protect(certInfo) {
416 | const userInfo = certInfo?.user
417 |
418 | if (userInfo && userInfo.email) {
419 | // NOTE - If logged > redirect to successPath OR previous path OR Home Page path
420 |
421 | return certInfo.successPath
422 | ? certInfo.successPath
423 | : certInfo.navigateInfo?.from
424 | ? certInfo.navigateInfo.from.fullPath
425 | : import.meta.env.ROUTER_HOME_PATH
426 | }
427 |
428 | return true
429 | },
430 | },
431 | }, // Login Page
432 | ```
433 |
434 | OK! You finish config protection for router, next I will show you how to use it
435 |
436 | Imagine that you go to Comment Page without login, and the system redirect you to Login Page. This requirement are resolved by the above configuration.
437 | In next step, in Login Page you click to login and after that the system has to redirect you go back Comment Page. This requirement are also resolved by the above configuration, but you must re-run the **protect()** in Login Page after login successfully. To do that, I have handled it and gave you a useful in API composition **useRoute** called **reProtect()**, all you need to do is just use it. See code below.
438 |
439 | ```javascript
440 | // LoginPage.tsx
441 | import { useUserInfo } from 'context/UserInfoContext'
442 |
443 | const route = useRoute()
444 | const { userInfo, setUserState } = useUserInfo()
445 |
446 | const onClickLogin = () => {
447 | setUserState({ ...userInfo, email: 'abc@gmail.com' })
448 |
449 | // NOTE - remember use Optional chaining "?.". Thanks to ES6 useful
450 | // Because the system don't know what routes have protect and what routes don't have
451 | route.handle.reProtect?.()
452 | }
453 | ```
454 |
455 | And finish! You finish the requirement about login success with just 1 line of code.
456 | But! wait minutes! We have an extensibility requirement
457 |
458 | ```markdown
459 | // Logout rules
460 | After login successfully
461 | The "user's email" and "Logout" label will display in header at right corner
462 |
463 | If user click "Logout" label
464 |
465 | 1. The system will logout account.
466 | 2. Next the system will check protect of current route.
467 | 3. If current route does not have protect rule or protect rule is valid,
468 | then do nothing.
469 | 4. If protect of current route return invalid,
470 | the system will redirect user to the verify route.
471 | ```
472 |
473 | I think you have already known what need to do. Correct! just use **reProtect()** after logout. See code below.
474 |
475 | ```javascript
476 | // Layout.tsx
477 | import { useUserInfo } from 'context/UserInfoContext'
478 |
479 | const route = useRoute()
480 | const { userState, setUserState } = useUserInfo()
481 |
482 | const onClickLogout = () => {
483 | setUserState({ ...userState, email: '' })
484 |
485 | // NOTE - remember use Optional chaining "?.". Thanks to ES6 useful
486 | // Because the system don't know what routes have protect and what routes don't have
487 | route.handle.reProtect?.()
488 | }
489 | ```
490 |
491 | Finish him! Easy to finish the extensibility requirement, jsut only 1 line of code.
492 |
493 | **NOTE**
494 |
495 | - Makesure your protect function is a **Pure Function**, it make your result will always right.
496 | - You can customize or implement your logic to handle protect case by using
497 |
498 | 1. **config/router/utils/RouterProtection.ts** to customize or implement logic.
499 | 2. **config/router/index.ts** to init your handler.
500 |
--------------------------------------------------------------------------------