├── 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 | 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 | '