├── src ├── api │ ├── .gitkeep │ ├── generic │ │ ├── index.js │ │ └── controller.js │ ├── comment │ │ ├── index.js │ │ └── controller.js │ ├── index.js │ ├── auth │ │ ├── index.js │ │ └── controller.js │ └── post │ │ ├── index.js │ │ └── controller.js ├── client │ ├── components │ │ ├── .gitkeep │ │ ├── Loading │ │ │ ├── index.js │ │ │ ├── styles.scss │ │ │ └── __tests__ │ │ │ │ └── Loading.test.js │ │ ├── MdViewer │ │ │ ├── __tests__ │ │ │ │ └── MdViewer.test.js │ │ │ └── index.js │ │ ├── TagsInput │ │ │ ├── __tests__ │ │ │ │ └── TagsInput.test.js │ │ │ └── index.js │ │ └── Layout │ │ │ ├── styles.scss │ │ │ └── index.js │ ├── vendor │ │ ├── bootstrap.js │ │ ├── react-mde.js │ │ ├── fontawesome.js │ │ ├── index.js │ │ └── react-toastify.js │ ├── assets │ │ └── image.png │ ├── themes │ │ ├── variables.scss │ │ └── global.scss │ ├── pages │ │ ├── Introduce │ │ │ ├── styles.scss │ │ │ ├── Projects │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── profile.js │ │ ├── NotFound │ │ │ ├── styles.scss │ │ │ └── index.js │ │ ├── Register │ │ │ ├── reducer.js │ │ │ ├── action.js │ │ │ └── index.js │ │ ├── Login │ │ │ ├── reducer.js │ │ │ ├── action.js │ │ │ └── index.js │ │ ├── Post │ │ │ ├── CreatePost │ │ │ │ ├── reducer.js │ │ │ │ ├── action.js │ │ │ │ ├── styles.scss │ │ │ │ └── index.js │ │ │ ├── action.js │ │ │ ├── reducer.js │ │ │ ├── PostDetail │ │ │ │ ├── reducer.js │ │ │ │ ├── action.js │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── styles.scss │ │ └── Contact │ │ │ └── index.js │ ├── stories │ │ ├── loading.stories.js │ │ └── index.stories.js │ ├── app │ │ └── index.js │ └── index.js ├── tools │ ├── jest │ │ ├── assets-mock.js │ │ └── styles-mock.js │ ├── postcss.config.js │ ├── webpack │ │ ├── webpack.config.js │ │ ├── webpack.config.dev.js │ │ ├── helper.js │ │ └── webpack.config.prod.js │ └── hooks.js ├── utils │ ├── cookies.js │ ├── index.js │ ├── head.js │ ├── render-html.js │ └── request.js ├── types │ ├── globals.js │ └── index.js ├── config.js ├── middlewares │ ├── notFoundError.middleware.js │ ├── index.js │ ├── serverError.middleware.js │ ├── passport.middleware.js │ └── webpack.middleware.js ├── index.js ├── i18n.js ├── secure │ ├── jwt.js │ ├── jwt.public.key │ ├── passport.js │ └── jwt.private.key ├── ready.js ├── mongo │ ├── helper.js │ └── index.js ├── store │ ├── index.js │ ├── reducer.js │ └── action.js ├── models │ └── result.model.js ├── routes.js └── server.js ├── public ├── robots.txt ├── googledb37d62693032295.html ├── assets │ ├── .DS_Store │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-192x192.png │ ├── favicon-32x32.png │ ├── favicon-512x512.png │ ├── apple-touch-icon.png │ └── safari-pinned-tab.svg └── locales │ ├── en-us │ └── common.json │ ├── en │ └── common.json │ └── vi │ └── common.json ├── .storybook ├── webpack.config.js └── config.js ├── .prettierrc ├── .env-cmdrc.json ├── .vscode └── launch.json ├── .github ├── workflows │ └── build.yml └── funding.yml ├── static.json ├── jsconfig.json ├── .babelrc ├── LICENSE ├── .flowconfig ├── .gitignore ├── .eslintrc.json ├── bin └── index.js ├── package.json └── README.md /src/api/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/tools/jest/assets-mock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'assets-mock'; 2 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/client/vendor/bootstrap.js: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.min.css'; 2 | -------------------------------------------------------------------------------- /public/googledb37d62693032295.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googledb37d62693032295.html -------------------------------------------------------------------------------- /src/client/vendor/react-mde.js: -------------------------------------------------------------------------------- 1 | import 'react-mde/lib/styles/css/react-mde-all.css'; 2 | -------------------------------------------------------------------------------- /public/assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/.DS_Store -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('../src/tools/webpack.config'); 2 | 3 | module.exports = webpack; 4 | -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/favicon.ico -------------------------------------------------------------------------------- /src/tools/jest/styles-mock.js: -------------------------------------------------------------------------------- 1 | import jestCSSModules from 'jest-css-modules'; 2 | 3 | export default jestCSSModules; 4 | -------------------------------------------------------------------------------- /src/client/assets/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/src/client/assets/image.png -------------------------------------------------------------------------------- /public/locales/en-us/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Language", 3 | "lang_en": "English", 4 | "lang_vi": "Vietnamese" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Language", 3 | "lang_en": "English", 4 | "lang_vi": "Vietnamese" 5 | } 6 | -------------------------------------------------------------------------------- /public/locales/vi/common.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "Ngôn ngữ", 3 | "lang_en": "Tiếng anh", 4 | "lang_vi": "Tiếng việt" 5 | } 6 | -------------------------------------------------------------------------------- /public/assets/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/favicon-16x16.png -------------------------------------------------------------------------------- /public/assets/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/favicon-192x192.png -------------------------------------------------------------------------------- /public/assets/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/favicon-32x32.png -------------------------------------------------------------------------------- /public/assets/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/favicon-512x512.png -------------------------------------------------------------------------------- /src/utils/cookies.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'universal-cookie'; 2 | 3 | const cookies = new Cookies(); 4 | 5 | export default cookies; 6 | -------------------------------------------------------------------------------- /public/assets/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htdangkhoa/express-react-boilerplate/HEAD/public/assets/apple-touch-icon.png -------------------------------------------------------------------------------- /src/client/vendor/fontawesome.js: -------------------------------------------------------------------------------- 1 | import '@fortawesome/fontawesome-free/css/all.min.css'; 2 | import '@fortawesome/fontawesome-free/js/all.min'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react'; 2 | 3 | configure(require.context('../src', true, /\.stories\.js$/), module); 4 | -------------------------------------------------------------------------------- /src/tools/postcss.config.js: -------------------------------------------------------------------------------- 1 | const autoprefixer = require('autoprefixer'); 2 | 3 | module.exports = { 4 | plugins: [autoprefixer({ grid: true })], 5 | }; 6 | -------------------------------------------------------------------------------- /src/client/vendor/index.js: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | import './fontawesome'; 3 | import './react-toastify'; 4 | import './react-mde'; 5 | import '../themes/global.scss'; 6 | -------------------------------------------------------------------------------- /src/client/vendor/react-toastify.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import 'react-toastify/dist/ReactToastify.css'; 3 | 4 | toast.configure({ autoClose: 8000 }); 5 | -------------------------------------------------------------------------------- /src/client/themes/variables.scss: -------------------------------------------------------------------------------- 1 | $ghost__white: #fbfbff; 2 | $celestial__blue: #449dd1; 3 | $steel__blue: #3b8ab8; 4 | $raisin__black: #222725; 5 | $arsenic: #444444; 6 | $silver__sand: #c5c5c5; 7 | -------------------------------------------------------------------------------- /src/client/pages/Introduce/styles.scss: -------------------------------------------------------------------------------- 1 | .btn__more { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | font-size: 1.5em; 6 | color: var(--primary__text); 7 | } 8 | -------------------------------------------------------------------------------- /src/tools/webpack/webpack.config.js: -------------------------------------------------------------------------------- 1 | import { isDev } from '../../config'; 2 | 3 | export default isDev 4 | ? require('./webpack.config.dev').default 5 | : require('./webpack.config.prod').default; 6 | -------------------------------------------------------------------------------- /src/client/stories/loading.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Loading from 'components/Loading'; 3 | 4 | export const withLoading = () => ; 5 | 6 | export default { title: 'Loading' }; 7 | -------------------------------------------------------------------------------- /src/api/generic/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { meController } from './controller'; 3 | 4 | const router = Router(); 5 | 6 | router.all('/me', meController()); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "semi": true, 6 | "bracketSpacing": true, 7 | "jsxSingleQuote": true, 8 | "jsxBracketSameLine": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /src/client/pages/NotFound/styles.scss: -------------------------------------------------------------------------------- 1 | .not__found__container { 2 | display: flex; 3 | height: 100%; 4 | justify-content: center; 5 | align-items: center; 6 | 7 | h1 { 8 | font-size: 5.5rem; 9 | } 10 | 11 | p { 12 | font-size: 1.5rem; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/globals.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | declare var __DEV__: boolean; 3 | 4 | declare var __SERVER__: boolean; 5 | 6 | declare var __CLIENT__: boolean; 7 | 8 | declare var module: { 9 | hot: { 10 | accept(path: string, callback: () => void | Promise): void, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/client/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RingLoader } from 'react-spinners'; 3 | import styles from './styles.scss'; 4 | 5 | const Loading = () => ( 6 |
7 | 8 |
9 | ); 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /src/api/generic/controller.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response } from 'express'; 3 | import { resultModel } from 'models/result.model'; 4 | 5 | export const meController = () => async (req: Request, res: Response) => { 6 | const { user } = req; 7 | 8 | return res.json(resultModel({ data: user })); 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import moment from 'moment-timezone'; 3 | 4 | export const actionGenerator = (actionName: string) => ({ 5 | NAME: actionName, 6 | SUCCESS: `${actionName}_SUCCESS`, 7 | ERROR: `${actionName}_ERROR`, 8 | }); 9 | 10 | export const formatDate = (date: Date | string) => 11 | moment(date).format('MMM DD, YYYY'); 12 | -------------------------------------------------------------------------------- /src/api/comment/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { Router } from 'express'; 3 | import { getCommentsController, postCommentController } from './controller'; 4 | 5 | const router = Router(); 6 | 7 | router.get('/get-comments/:_id', getCommentsController()); 8 | 9 | router.post('/post-comment', postCommentController()); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const { 2 | NODE_ENV = 'development', 3 | PORT = 8080, 4 | DB_HOST, 5 | DB_NAME, 6 | DB_USER, 7 | DB_PASS, 8 | } = process.env; 9 | 10 | const isDev = NODE_ENV !== 'production'; 11 | 12 | global.__DEV__ = isDev; 13 | 14 | module.exports = { 15 | NODE_ENV, 16 | PORT, 17 | isDev, 18 | DB_HOST, 19 | DB_NAME, 20 | DB_USER, 21 | DB_PASS, 22 | }; 23 | -------------------------------------------------------------------------------- /src/client/stories/index.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@storybook/react/demo'; 3 | 4 | export const withText = () => ; 5 | 6 | export const withEmoji = () => ( 7 | 12 | ); 13 | 14 | export default { title: 'Button' }; 15 | -------------------------------------------------------------------------------- /src/middlewares/notFoundError.middleware.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response, type NextFunction } from 'express'; 3 | import { notFoundError } from 'models/result.model'; 4 | 5 | const notFoundErrorMiddleware = () => async ( 6 | req: Request, 7 | res: Response, 8 | _next: NextFunction, 9 | ) => res.json(notFoundError()); 10 | 11 | export default notFoundErrorMiddleware; 12 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import auth from './auth'; 3 | import generic from './generic'; 4 | import post from './post'; 5 | import comment from './comment'; 6 | 7 | const router = Router(); 8 | 9 | router.use('/auth', auth); 10 | 11 | router.use('/post', post); 12 | 13 | router.use('/comment', comment); 14 | 15 | router.use('/', [generic]); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /src/client/components/Loading/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'themes/variables'; 2 | 3 | .app__loading { 4 | background-color: rgba(0, 0, 0, 0.7); 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | bottom: 0; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | 14 | div { 15 | div { 16 | border: 6px solid $celestial__blue; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import webpackMiddleware from './webpack.middleware'; 2 | import passportMiddleware from './passport.middleware'; 3 | import notFoundErrorMiddleware from './notFoundError.middleware'; 4 | import serverErrorMiddleware from './serverError.middleware'; 5 | 6 | export { 7 | webpackMiddleware, 8 | passportMiddleware, 9 | notFoundErrorMiddleware, 10 | serverErrorMiddleware, 11 | }; 12 | -------------------------------------------------------------------------------- /src/api/auth/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | registerController, 4 | loginController, 5 | renewTokenController, 6 | } from './controller'; 7 | 8 | const router = Router(); 9 | 10 | router.post('/register', registerController()); 11 | 12 | router.post('/login', loginController()); 13 | 14 | router.post('/renew-token', renewTokenController()); 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /.env-cmdrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "NODE_ENV": "development", 4 | "DB_HOST": "mongodb://localhost:27017", 5 | "DB_NAME": "erb", 6 | "DB_USER": "", 7 | "DB_PASS": "" 8 | }, 9 | "production": { 10 | "NODE_ENV": "production", 11 | "DB_HOST": "mongodb+srv://cluster0-pyhnn.mongodb.net/", 12 | "DB_NAME": "erb", 13 | "DB_USER": "admin", 14 | "DB_PASS": "admin" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/api/post/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { 3 | getPostsController, 4 | createPostController, 5 | getPostDetailController, 6 | } from './controller'; 7 | 8 | const router = Router(); 9 | 10 | router.all('/newest', getPostsController()); 11 | 12 | router.all('/detail/:_id', getPostDetailController()); 13 | 14 | router.post('/create-post', createPostController()); 15 | 16 | export default router; 17 | -------------------------------------------------------------------------------- /src/client/pages/Register/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type ActionType } from 'types'; 3 | import { REGISTER_ACTION } from './action'; 4 | 5 | const initialState = { 6 | registerSuccess: false, 7 | }; 8 | 9 | export default (state: Object = initialState, action: ActionType) => { 10 | switch (action.type) { 11 | case REGISTER_ACTION.SUCCESS: { 12 | return { ...state, registerSuccess: true }; 13 | } 14 | default: 15 | return { ...state }; 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | let port = parseFloat(process.env.PORT || 8080); 3 | 4 | if (process.env.NODE_ENV !== 'production') { 5 | const { default: portChecker } = await import('tcp-port-used'); 6 | 7 | let isUsed = true; 8 | 9 | while (isUsed) { 10 | /* eslint-disable no-await-in-loop */ 11 | isUsed = await portChecker.check(port); 12 | 13 | port += 1; 14 | } 15 | } 16 | 17 | process.env.PORT = port; 18 | 19 | await import('./ready'); 20 | })(); 21 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Debug", 9 | "type": "node", 10 | "request": "attach", 11 | "port": 58585, 12 | "restart": true, 13 | "sourceMaps": true, 14 | "timeout": 60000 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/client/components/Loading/__tests__/Loading.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import Loading from '../index'; 6 | 7 | describe('', () => { 8 | it('renders', () => { 9 | const tree = renderer 10 | .create( 11 | 12 | 13 | , 14 | ) 15 | .toJSON(); 16 | 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Github Actions 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - develop 7 | - release/* 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-18.04 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Setup Node.js for use with actions 16 | uses: actions/setup-node@v1.1.0 17 | with: 18 | node-version: "10.x" 19 | - run: yarn install 20 | - run: yarn build 21 | env: 22 | NODE_ENV: production 23 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "dist", 3 | "clean_urls": true, 4 | "routes": { 5 | "/**/*.js": "/", 6 | "/**/*.gz": "/", 7 | "/**/*.css": "/", 8 | "/**/*.woff": "/", 9 | "/**/*.woff2": "/", 10 | "/**/*.ttf": "/", 11 | "/**/*.eot": "/", 12 | "/**/*.svg": "/", 13 | "/**/*.gif": "/", 14 | "/**/*.png": "/", 15 | "/**/*.jpg": "/", 16 | "/**/*.jpeg": "/", 17 | "/**/*.webp": "/", 18 | "/**/*.ico": "/", 19 | "/**/*.txt": "/", 20 | "/**/*.json": "/", 21 | "/**/*.html": "/" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/client/components/MdViewer/__tests__/MdViewer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import MdViewer from '../index'; 6 | 7 | describe('', () => { 8 | it('renders', () => { 9 | const tree = renderer 10 | .create( 11 | 12 | 13 | , 14 | ) 15 | .toJSON(); 16 | 17 | expect(tree).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/client/pages/NotFound/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Layout from 'components/Layout'; 4 | 5 | import './styles.scss'; 6 | 7 | const NotFound = ({ route: { title }, staticContext = {} }) => { 8 | staticContext.status = '404'; 9 | 10 | return ( 11 | 12 |
13 |

404

14 |

The page you're looking for isn't here.

15 |
16 |
17 | ); 18 | }; 19 | 20 | export default NotFound; 21 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import Backend from 'i18next-xhr-backend'; 3 | import LanguageDetector from 'i18next-browser-languagedetector'; 4 | import { initReactI18next } from 'react-i18next'; 5 | 6 | i18next 7 | .use(LanguageDetector) 8 | .use(Backend) 9 | .use(initReactI18next) 10 | .init({ 11 | ns: ['common'], 12 | defaultNS: 'common', 13 | fallbackLng: ['en'], 14 | debug: __DEV__, 15 | interpolation: { 16 | escapeValue: false, 17 | }, 18 | backend: { 19 | loadPath: '/locales/{{lng}}/{{ns}}.json', 20 | }, 21 | }); 22 | 23 | export default i18next; 24 | -------------------------------------------------------------------------------- /src/client/components/TagsInput/__tests__/TagsInput.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | 5 | import TagsInput from '../index'; 6 | 7 | describe('', () => { 8 | it('renders', () => { 9 | const tree = renderer 10 | .create( 11 | 12 | { 14 | console.log(tag); 15 | }} 16 | /> 17 | , 18 | ) 19 | .toJSON(); 20 | 21 | expect(tree).toMatchSnapshot(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/client/pages/Login/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type ActionType } from 'types'; 3 | import { LOGIN_ACTION } from './action'; 4 | 5 | const initialState = { 6 | data: null, 7 | error: null, 8 | }; 9 | 10 | export default (state: any = initialState, action: ActionType) => { 11 | switch (action.type) { 12 | case LOGIN_ACTION.SUCCESS: { 13 | const { data } = action.payload; 14 | 15 | return { ...state, ...data }; 16 | } 17 | case LOGIN_ACTION.ERROR: { 18 | const { error } = action.payload; 19 | 20 | return { ...state, error }; 21 | } 22 | default: 23 | return { ...state }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: htdangkhoa 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: htdangkhoa 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ["https://www.buymeacoffee.com/udXILEJ"] 13 | -------------------------------------------------------------------------------- /src/client/pages/Login/action.js: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import { actionGenerator } from 'utils'; 3 | import { requestAction } from 'utils/request'; 4 | import { updateTokenAction } from 'store/action'; 5 | 6 | export const LOGIN_ACTION = actionGenerator('@@LOGIN_ACTION'); 7 | export const loginAction = (data) => (dispatch) => 8 | dispatch( 9 | requestAction({ 10 | url: '/auth/login', 11 | label: LOGIN_ACTION.NAME, 12 | method: 'POST', 13 | data, 14 | onSuccess: ({ data: res }) => { 15 | dispatch(updateTokenAction({ ...res })); 16 | }, 17 | onError: ({ error }) => { 18 | toast.error(error.message); 19 | }, 20 | }), 21 | ); 22 | -------------------------------------------------------------------------------- /src/tools/webpack/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import WebpackBar from 'webpackbar'; 3 | import FriendlyErrorsWebpackPlugin from 'friendly-errors-webpack-plugin'; 4 | 5 | import { 6 | getEntries, 7 | getOutPut, 8 | getPlugins, 9 | getRules, 10 | getResolver, 11 | } from './helper'; 12 | 13 | export default { 14 | mode: 'development', 15 | devtool: 'source-map', 16 | entry: getEntries(), 17 | output: getOutPut(), 18 | plugins: [ 19 | ...getPlugins(), 20 | new WebpackBar(), 21 | new webpack.HotModuleReplacementPlugin(), 22 | new FriendlyErrorsWebpackPlugin(), 23 | ], 24 | module: { 25 | rules: getRules(), 26 | }, 27 | ...getResolver(), 28 | }; 29 | -------------------------------------------------------------------------------- /src/client/pages/Post/CreatePost/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { ActionType } from 'types'; 3 | import { CREATE_POST, DELETE_LOCAL_POST } from './action'; 4 | 5 | const initialState = { 6 | post: null, 7 | error: null, 8 | }; 9 | 10 | export default (state: any = initialState, action: ActionType) => { 11 | switch (action.type) { 12 | case CREATE_POST.SUCCESS: { 13 | return { ...state, post: action.payload, error: null }; 14 | } 15 | case CREATE_POST.ERROR: { 16 | return { ...state, payload: null, error: action.payload }; 17 | } 18 | case DELETE_LOCAL_POST: { 19 | return { ...state, error: null, post: null }; 20 | } 21 | default: 22 | return state; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /src/secure/jwt.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { resolve } from 'path'; 3 | import { readFileSync } from 'fs'; 4 | import jwt from 'jsonwebtoken'; 5 | 6 | export const PUBLIC_KEY = readFileSync( 7 | resolve(__dirname, 'jwt.public.key'), 8 | 'utf8', 9 | ); 10 | 11 | const PRIVATE_KEY = readFileSync(resolve(__dirname, 'jwt.private.key'), 'utf8'); 12 | 13 | export const TYPE_ACCESS = '@@TYPE_ACCESS'; 14 | 15 | export const TYPE_REFRESH = '@@TYPE_REFRESH'; 16 | 17 | export const sign = ( 18 | payload: string | Object, 19 | expiresIn?: string | number = 86400, 20 | ): string => jwt.sign(payload, PRIVATE_KEY, { expiresIn, algorithm: 'RS256' }); 21 | 22 | export const verify = (token: string): any => 23 | jwt.verify(token, PUBLIC_KEY, { algorithms: 'RS256' }); 24 | -------------------------------------------------------------------------------- /src/middlewares/serverError.middleware.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response, type NextFunction } from 'express'; 3 | import isArray from 'lodash/isArray'; 4 | import { internalServerError } from 'models/result.model'; 5 | 6 | const serverErrorMiddleware = () => async ( 7 | err: any, 8 | _req: Request, 9 | res: Response, 10 | _next: NextFunction, 11 | ) => { 12 | let message = ''; 13 | 14 | if (isArray(err.error)) { 15 | message = err.error.map((e) => `${e.param}: ${e.msg}`).toString(); 16 | } else { 17 | message = !err.error 18 | ? err.message 19 | : `${err.error.message} ${err.error.detail || ''}`; 20 | } 21 | 22 | return res.json(internalServerError({ message })); 23 | }; 24 | 25 | export default serverErrorMiddleware; 26 | -------------------------------------------------------------------------------- /src/client/pages/Post/action.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Dispatch } from 'redux'; 3 | import { type ApiDataType } from 'types'; 4 | import { actionGenerator } from 'utils'; 5 | import { requestAction } from 'utils/request'; 6 | 7 | export const GET_POSTS = actionGenerator('@@GET_POSTS'); 8 | 9 | export const getPostsAction = (skip?: number = 0) => (dispatch: Dispatch) => 10 | dispatch( 11 | requestAction({ 12 | url: '/post/newest', 13 | label: GET_POSTS.NAME, 14 | params: { skip }, 15 | onSuccess: ({ data }: ApiDataType) => { 16 | dispatch({ type: GET_POSTS.SUCCESS, payload: data }); 17 | }, 18 | onError: ({ error }: ApiDataType) => { 19 | dispatch({ type: GET_POSTS.ERROR, payload: error }); 20 | }, 21 | }), 22 | ); 23 | -------------------------------------------------------------------------------- /src/client/pages/Register/action.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Dispatch } from 'redux'; 3 | import { toast } from 'react-toastify'; 4 | import { type ApiDataType } from 'types'; 5 | import { actionGenerator } from 'utils'; 6 | import { requestAction } from 'utils/request'; 7 | 8 | export const REGISTER_ACTION = actionGenerator('@@REGISTER_ACTION'); 9 | export const registerAction = (data: Object) => (dispatch: Dispatch) => 10 | dispatch( 11 | requestAction({ 12 | url: '/auth/register', 13 | label: REGISTER_ACTION.NAME, 14 | method: 'POST', 15 | data, 16 | onSuccess: (_res: ApiDataType) => 17 | dispatch({ type: REGISTER_ACTION.SUCCESS }), 18 | onError: ({ error }: ApiDataType) => { 19 | toast.error(error?.message); 20 | }, 21 | }), 22 | ); 23 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "noImplicitAny": true, 5 | "allowSyntheticDefaultImports": true, 6 | "esModuleInterop": true, 7 | "module": "commonjs", 8 | "target": "es6", 9 | "jsx": "react", 10 | "baseUrl": "src", 11 | "paths": { 12 | "pages": ["client/pages"], 13 | "pages/*": ["client/pages/*"], 14 | "components": ["client/components"], 15 | "components/*": ["client/components/*"], 16 | "assets/*": ["client/assets/*"], 17 | "utils": ["utils"], 18 | "utils/*": ["utils/*"], 19 | "types": ["types"], 20 | "types/*": ["types/*"], 21 | "models": ["models"], 22 | "models/*": ["models/*"], 23 | "store": ["store"], 24 | "store/*": ["store/*"] 25 | } 26 | }, 27 | "include": ["src"] 28 | } 29 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react", "@babel/flow"], 3 | "plugins": [ 4 | "@babel/syntax-dynamic-import", 5 | "@babel/transform-runtime", 6 | [ 7 | "@babel/transform-spread", 8 | { 9 | "loose": true 10 | } 11 | ], 12 | "@babel/proposal-optional-chaining", 13 | "react-hot-loader/babel", 14 | "@loadable/babel-plugin", 15 | [ 16 | "module-resolver", 17 | { 18 | "root": ["./src"], 19 | "alias": { 20 | "pages": "./src/client/pages", 21 | "components": "./src/client/components", 22 | "assets": "./src/client/assets", 23 | "utils": "./src/utils", 24 | "types": "./src/types", 25 | "models": "./src/models", 26 | "store": "./src/store" 27 | } 28 | } 29 | ] 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/ready.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { 3 | NODE_ENV, 4 | PORT, 5 | DB_HOST as host, 6 | DB_NAME as database, 7 | DB_USER as user, 8 | DB_PASS as password, 9 | } from './config'; 10 | import hooks from './tools/hooks'; 11 | import useMongo from './mongo'; 12 | 13 | global.__CLIENT__ = false; 14 | global.__SERVER__ = true; 15 | 16 | (async () => { 17 | hooks(); 18 | 19 | try { 20 | const { default: server } = await import('./server'); 21 | 22 | await useMongo({ 23 | host, 24 | database, 25 | user, 26 | password, 27 | app: server, 28 | }); 29 | 30 | createServer(server).listen(PORT, () => { 31 | console.log(`Starting the ${NODE_ENV} server...`); 32 | }); 33 | } catch (error) { 34 | console.error(error); 35 | 36 | process.exit(1); 37 | } 38 | })(); 39 | -------------------------------------------------------------------------------- /src/secure/jwt.public.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyVDcJwZfwwAuVg9O02sw 3 | oK0q5ryTvtCSfRwKmbdqOVfrHkqPD04BcinrcfJbcK2uwBeys00cpF+maVJcZG6K 4 | aUDOMaJRAfKD7pi6LayCRZlnxWeFE5INLfbRobajuivGfMq+5Gv7Hp++yvCQZWXO 5 | x3YxmZqYvKjM2BC3eLz1s+1UoCE80MAHCbP0+qkyskaZMgUGrp//nxB8t2rZYBIl 6 | 7c7qZ6v2OElHmwOXKHbNdlxK+PBtKUy1AyGhm6vJJ+aLHPO9b/gNQPgEDEZ5NjxA 7 | G1d1a6KSzjFqjKY+kvEPKMLjOChRIGuoZ5dwv66hdRREHtEvAb08vLcabc/4uj7u 8 | c8LkVBDCUhHOhUzcOWFwKxpglnpJoNecU/sPdq+dnlsfzZz2G8cfh7FTJzo97cht 9 | r83L5aMafJ8+CMvEN/LRO1erUdbVDrPqN0OoNh4h3tF15p9Kjzm7bnm6R7NNH7yV 10 | 2XcLUTbzFzBdf58if+OEVGLsqLUnw/mHz19kEIdOi76tm2ZofoN2CjxMWzjr4PRM 11 | DixfY50faP8xlr35uUGPo484fH1nc3N1qA5rCIGn1JUSR8zh4HuMUfiDbOGglvz4 12 | LdBRHvJEUwNQipADTm4kxnwb5N3bagc2K2dySU2f918AAW6TTOzRPrUKkCBz6eVR 13 | Zv5XNPWUxLP4jDmtiFOK/7kCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /src/client/pages/Introduce/Projects/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import chunk from 'lodash/chunk'; 3 | 4 | import Layout from 'components/Layout'; 5 | import MdViewer from 'components/MdViewer'; 6 | 7 | import { projects } from '../profile'; 8 | 9 | const projectsGrouped = chunk(projects, 2); 10 | 11 | const Projects = ({ route: { title } }) => { 12 | return ( 13 | 14 | 15 | 16 | {projectsGrouped.map((group, i) => { 17 | return ( 18 |
19 | {group.map((item, j) => { 20 | return ( 21 |
22 | 23 |
24 | ); 25 | })} 26 |
27 | ); 28 | })} 29 |
30 | ); 31 | }; 32 | 33 | export default Projects; 34 | -------------------------------------------------------------------------------- /src/utils/head.js: -------------------------------------------------------------------------------- 1 | export default { 2 | htmlAttributes: { 3 | lang: 'en', 4 | }, 5 | defaultTitle: 'Express React Boilerplate', 6 | titleTemplate: 'Express React Boilerplate | %s', 7 | link: [ 8 | { 9 | rel: 'canonical', 10 | href: __DEV__ 11 | ? 'http://localhost:8080' 12 | : 'https://htdangkhoa-erb.herokuapp.com/', 13 | }, 14 | ], 15 | meta: [ 16 | { 17 | name: 'theme-color', 18 | content: '#222725', 19 | }, 20 | { 21 | name: 'description', 22 | content: 23 | '🔥 This is a tool that helps programmers create Express & React projects easily.', 24 | }, 25 | { 26 | name: 'keywords', 27 | content: 28 | 'kblog, erb, erb-gen, express, expressjs, rest, restful, router, app, api, react, react-router, redux, template, webpack, universal, boilerplate', 29 | }, 30 | { 31 | name: 'author', 32 | content: 'htdangkhoa', 33 | }, 34 | ], 35 | }; 36 | -------------------------------------------------------------------------------- /src/client/pages/Post/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { combineReducers } from 'redux'; 3 | import { type ActionType } from 'types'; 4 | import { GET_POSTS } from './action'; 5 | import postDetail from './PostDetail/reducer'; 6 | import createPost from './CreatePost/reducer'; 7 | 8 | const initialState = { 9 | posts: [], 10 | metaData: { 11 | index: 0, 12 | total: 0, 13 | }, 14 | error: null, 15 | }; 16 | 17 | const post = (state: any = initialState, action: ActionType) => { 18 | switch (action.type) { 19 | case GET_POSTS.SUCCESS: { 20 | const { posts, metaData } = action.payload; 21 | 22 | return { 23 | ...state, 24 | posts: [...posts], 25 | metaData: { ...metaData }, 26 | }; 27 | } 28 | case GET_POSTS.ERROR: { 29 | return { ...state, error: action.payload }; 30 | } 31 | default: 32 | return { ...state }; 33 | } 34 | }; 35 | 36 | export default combineReducers({ post, postDetail, createPost }); 37 | -------------------------------------------------------------------------------- /src/middlewares/passport.middleware.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response, type NextFunction } from 'express'; 3 | import { unauthorized } from 'models/result.model'; 4 | import passport from '../secure/passport'; 5 | 6 | const passportMiddleware = (whiteList: string[] = []) => async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction, 10 | ) => { 11 | const matchRoutes = 12 | whiteList 13 | .map((condition) => req.originalUrl.match(condition)) 14 | .filter((item) => item !== null) || []; 15 | 16 | if (matchRoutes.length !== 0) { 17 | return next(); 18 | } 19 | 20 | return passport.authenticate('jwt', { session: false }, (error, result) => { 21 | if (!result) return res.status(401).json(unauthorized()); 22 | 23 | if (result.code !== 200) return res.status(401).json(result); 24 | 25 | req.user = result.data; 26 | 27 | return next(); 28 | })(req, res, next); 29 | }; 30 | 31 | export default passportMiddleware; 32 | -------------------------------------------------------------------------------- /src/client/pages/Post/CreatePost/action.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { Dispatch } from 'redux'; 3 | import type { ApiDataType } from 'types'; 4 | import { actionGenerator } from 'utils'; 5 | import { requestAction } from 'utils/request'; 6 | 7 | export const CREATE_POST = actionGenerator('@@CREATE_POST'); 8 | 9 | export const createPostAction = (data: Object) => (dispatch: Dispatch) => 10 | dispatch( 11 | requestAction({ 12 | url: '/post/create-post', 13 | label: CREATE_POST.NAME, 14 | method: 'POST', 15 | data, 16 | onSuccess: ({ data: res }: ApiDataType) => { 17 | dispatch({ type: CREATE_POST.SUCCESS, payload: res }); 18 | }, 19 | onError: ({ error }: ApiDataType) => { 20 | dispatch({ type: CREATE_POST.ERROR, payload: error }); 21 | }, 22 | }), 23 | ); 24 | 25 | export const DELETE_LOCAL_POST = '@@DELETE_LOCAL_POST'; 26 | 27 | export const deleteLocalPostAction = () => (dispatch: Dispatch) => 28 | dispatch({ type: DELETE_LOCAL_POST, payload: null }); 29 | -------------------------------------------------------------------------------- /src/client/pages/Contact/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Layout from 'components/Layout'; 4 | import MdViewer from 'components/MdViewer'; 5 | 6 | const source = ` 7 | # Contact 8 | 9 | KBlog is a website to share experiences, knowledge, case studies to help you create more professional applications and products. 10 | 11 | KBlog always strives to improve the content and quality of the article to provide you with the most useful information and knowledge in the field of programming and design to be able to create more professional applications. 12 | 13 | KBlog is ready to welcome cooperation opportunities with all of you for the opportunity to develop more. 14 | 15 | All information should be answered questions, support you please contact me at the address below: 16 | 17 | Email: huynhtran.dangkhoa@gmail.com 18 | `; 19 | 20 | const Contact = ({ route: { title } }) => ( 21 | 22 | 23 | 24 | ); 25 | 26 | export default Contact; 27 | -------------------------------------------------------------------------------- /src/client/pages/Post/PostDetail/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type ActionType } from 'types'; 3 | import { GET_POST_DETAIL, GET_COMMENTS, POST_COMMENT } from './action'; 4 | 5 | const initialState = { 6 | post: null, 7 | comments: [], 8 | error: null, 9 | }; 10 | 11 | export default (state: any = initialState, action: ActionType) => { 12 | switch (action.type) { 13 | case GET_POST_DETAIL.SUCCESS: { 14 | return { ...state, post: action.payload }; 15 | } 16 | case GET_POST_DETAIL.ERROR: { 17 | return { ...state, error: action.payload }; 18 | } 19 | case GET_COMMENTS.SUCCESS: { 20 | return { ...state, comments: [...action.payload] }; 21 | } 22 | case GET_COMMENTS.ERROR: { 23 | return { ...state, error: { ...action.payload } }; 24 | } 25 | case POST_COMMENT.SUCCESS: { 26 | return { ...state, comments: [...state.comments, action.payload] }; 27 | } 28 | case POST_COMMENT.ERROR: { 29 | return { ...state, error: { ...action.payload } }; 30 | } 31 | default: 32 | return { ...state }; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/client/app/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { useEffect } from 'react'; 3 | import { Helmet } from 'react-helmet'; 4 | import { connect } from 'react-redux'; 5 | import { hot } from 'react-hot-loader'; 6 | import { renderRoutes } from 'react-router-config'; 7 | import { ToastContainer } from 'react-toastify'; 8 | import head from 'utils/head'; 9 | 10 | import Loading from 'components/Loading'; 11 | 12 | import * as globalAction from 'store/action'; 13 | 14 | const App = ({ route, global: { isLoading }, updateThemeAction }) => { 15 | useEffect(() => { 16 | updateThemeAction(localStorage.getItem('theme') || 'light'); 17 | }, []); 18 | 19 | return ( 20 | <> 21 | 22 | {renderRoutes(route.routes)} 23 | {isLoading && } 24 | 25 | 26 | ); 27 | }; 28 | 29 | const mapStateToProps = ({ global }) => ({ global }); 30 | 31 | const mapDispatchToProps = { 32 | updateThemeAction: globalAction.updateThemeAction, 33 | }; 34 | 35 | export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(App)); 36 | -------------------------------------------------------------------------------- /src/tools/hooks.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import cssModuleRequireHook from 'css-modules-require-hook'; 3 | import sass from 'node-sass'; 4 | import assetRequireHook from 'asset-require-hook'; 5 | import postcssConfig from './postcss.config'; 6 | 7 | const hooks = () => { 8 | cssModuleRequireHook({ 9 | generateScopedName: '[local]', 10 | extensions: ['.css', '.scss', '.sass'], 11 | prepend: [...postcssConfig.plugins], 12 | preprocessCss: (data, file) => { 13 | return sass.renderSync({ 14 | data, 15 | file, 16 | includePaths: [resolve(process.cwd(), 'src/client')], 17 | }).css; 18 | }, 19 | devMode: __DEV__, 20 | }); 21 | 22 | assetRequireHook({ 23 | extensions: ['gif', 'jpg', 'jpeg', 'png', 'webp'], 24 | publicPath: '/', 25 | limit: 10240, 26 | name: '[name].[hash:8].[ext]', 27 | }); 28 | 29 | assetRequireHook({ 30 | extensions: ['woff', 'woff2', 'ttf', 'eot', 'svg'], 31 | publicPath: '/', 32 | limit: 10240, 33 | name: '[name].[hash:8].[ext]', 34 | }); 35 | }; 36 | 37 | export default hooks; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Huỳnh Trần Đăng Khoa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/mongo/helper.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type MongoPagingType, type MongoPagingResultType } from 'types'; 3 | import head from 'lodash/head'; 4 | 5 | export const usePaging = async ({ 6 | collection, 7 | aggregate = [], 8 | skip = 0, 9 | limit = 10, 10 | }: MongoPagingType): MongoPagingResultType => { 11 | const roundSkip = Math.abs(Math.floor(skip)); 12 | 13 | const roundLimit = Math.abs(Math.floor(limit)); 14 | 15 | const realSkip = roundSkip * roundLimit; 16 | 17 | const list = await collection 18 | .aggregate([ 19 | { 20 | $facet: { 21 | total: [{ $count: 'count' }], 22 | data: [ 23 | ...aggregate, 24 | { $skip: realSkip }, 25 | { 26 | $limit: roundLimit, 27 | }, 28 | ], 29 | }, 30 | }, 31 | { $unwind: '$total' }, 32 | ]) 33 | .toArray(); 34 | 35 | const result = head(list); 36 | 37 | const { count = 0 } = result?.total; 38 | 39 | const total = Math.ceil(count / roundLimit); 40 | 41 | const metaData = { 42 | index: roundSkip, 43 | total, 44 | }; 45 | 46 | return { 47 | values: result?.data || [], 48 | metaData, 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /src/client/pages/Post/CreatePost/styles.scss: -------------------------------------------------------------------------------- 1 | @import '../styles'; 2 | 3 | .create__post__container { 4 | .form-control { 5 | margin: 12px 0; 6 | } 7 | 8 | .tags { 9 | &__group { 10 | display: flex; 11 | flex-wrap: wrap; 12 | margin-bottom: 12px; 13 | padding: 0; 14 | align-items: center; 15 | background: white; 16 | border: 1px solid #ced4da; 17 | border-radius: 4px; 18 | padding: 6px 12px; 19 | 20 | .tag__input__item { 21 | margin-bottom: 0; 22 | max-width: 100%; 23 | display: flex; 24 | align-items: center; 25 | 26 | span { 27 | max-width: 120px; 28 | overflow: hidden; 29 | text-overflow: ellipsis; 30 | } 31 | 32 | button:focus { 33 | outline: none; 34 | } 35 | 36 | button.close { 37 | .fa-times { 38 | width: 0.5em; 39 | } 40 | } 41 | } 42 | } 43 | 44 | &__input { 45 | border: none; 46 | outline: none; 47 | margin: 6px 0; 48 | flex: 1; 49 | display: block; 50 | color: #495057; 51 | } 52 | } 53 | } 54 | 55 | .btn-publish { 56 | margin: 24px 0; 57 | } 58 | -------------------------------------------------------------------------------- /src/secure/passport.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request } from 'express'; 3 | import { ObjectId } from 'mongodb'; 4 | import passport from 'passport'; 5 | import { 6 | Strategy as JWTStrategy, 7 | ExtractJwt, 8 | type VerifiedCallback, 9 | } from 'passport-jwt'; 10 | import { unauthorized, resultModel } from 'models/result.model'; 11 | import { PUBLIC_KEY, TYPE_ACCESS } from './jwt'; 12 | 13 | const jwtStrategy = new JWTStrategy( 14 | { 15 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 16 | secretOrKey: PUBLIC_KEY, 17 | passReqToCallback: true, 18 | }, 19 | async (req: Request, payload: any, done: VerifiedCallback) => { 20 | const { user, usersCollection } = req; 21 | 22 | if (user) { 23 | return done(null, resultModel({ data: user })); 24 | } 25 | 26 | if (payload.type !== TYPE_ACCESS) { 27 | return done(null, unauthorized()); 28 | } 29 | 30 | const document = await usersCollection.findOne({ 31 | _id: ObjectId(payload._id), 32 | }); 33 | 34 | if (!document) return done(null, unauthorized()); 35 | 36 | return done(null, resultModel({ data: document })); 37 | }, 38 | ); 39 | 40 | passport.use(jwtStrategy); 41 | 42 | export default passport; 43 | -------------------------------------------------------------------------------- /src/mongo/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { MongoClient } from 'mongodb'; 3 | import { type MongoConnectionType, type MongoResultType } from 'types'; 4 | 5 | const useMongo = ({ 6 | host, 7 | database, 8 | user, 9 | password, 10 | app, 11 | }: MongoConnectionType): Promise => 12 | MongoClient.connect(host, { 13 | useNewUrlParser: true, 14 | useUnifiedTopology: true, 15 | auth: !user || !password ? null : { user, password }, 16 | }) 17 | .then((client) => { 18 | const db = client.db(database); 19 | 20 | const usersCollection = db.collection('users'); 21 | 22 | const postsCollection = db.collection('posts'); 23 | 24 | const commentsCollection = db.collection('comments'); 25 | 26 | const result: MongoResultType = { 27 | client, 28 | db, 29 | }; 30 | 31 | if (app) { 32 | const { request } = app; 33 | 34 | Object.assign(request, { 35 | ...result, 36 | usersCollection, 37 | postsCollection, 38 | commentsCollection, 39 | }); 40 | } 41 | 42 | return result; 43 | }) 44 | .catch((error) => { 45 | throw error; 46 | }); 47 | 48 | export default useMongo; 49 | -------------------------------------------------------------------------------- /src/middlewares/webpack.middleware.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import webpackDevMiddleware from 'webpack-dev-middleware'; 3 | import webpackHotMiddleware from 'webpack-hot-middleware'; 4 | import openBrowser from 'react-dev-utils/openBrowser'; 5 | import { ip } from 'address'; 6 | import { PORT } from 'config'; 7 | import webpackConfig from '../tools/webpack/webpack.config'; 8 | 9 | const compiler = webpack(webpackConfig); 10 | 11 | const webpackMiddleware = () => { 12 | const instance = webpackDevMiddleware(compiler, { 13 | headers: { 'Access-Control-Allow-Origin': '*' }, 14 | publicPath: webpackConfig.output.publicPath, 15 | hot: true, 16 | quiet: true, // Turn it on for friendly-errors-webpack-plugin 17 | noInfo: true, 18 | stats: 'minimal', 19 | serverSideRender: true, 20 | }); 21 | 22 | instance.waitUntilValid(() => { 23 | const host = `http://${ip()}:${PORT}/`; 24 | 25 | console.log(`Server is serving at: ${host}`); 26 | 27 | if (process.argv.includes('--serve')) { 28 | openBrowser(host); 29 | } 30 | }); 31 | 32 | return [ 33 | instance, 34 | webpackHotMiddleware(compiler, { 35 | log: false, 36 | }), 37 | ]; 38 | }; 39 | 40 | export default webpackMiddleware; 41 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | tests 3 | node_modules 4 | dist 5 | public 6 | 7 | [include] 8 | 9 | [libs] 10 | src/types/globals.js 11 | 12 | [lints] 13 | 14 | [options] 15 | esproposal.optional_chaining=ignore 16 | esproposal.decorators=ignore 17 | module.system.node.resolve_dirname=node_modules 18 | module.system.node.resolve_dirname=src 19 | 20 | module.file_ext=.js 21 | module.file_ext=.jsx 22 | module.file_ext=.json 23 | module.file_ext=.woff 24 | module.file_ext=.woff2 25 | module.file_ext=.ttf 26 | module.file_ext=.eot 27 | module.file_ext=.svg 28 | module.file_ext=.gif 29 | module.file_ext=.png 30 | module.file_ext=.jpg 31 | module.file_ext=.jpeg 32 | module.file_ext=.webp 33 | module.file_ext=.css 34 | module.file_ext=.scss 35 | 36 | module.name_mapper='^pages\/\(.*\)$' -> '/src/client/pages/\1' 37 | module.name_mapper='^components\/\(.*\)$' -> '/src/client/components/\1' 38 | module.name_mapper='^assets\/\(.*\)$' -> '/src/client/assets/\1' 39 | module.name_mapper='^utils\/\(.*\)$' -> '/src/utils/\1' 40 | module.name_mapper='^types\/\(.*\)$' -> '/src/types/\1' 41 | module.name_mapper='^models\/\(.*\)$' -> '/src/models/\1' 42 | module.name_mapper='^store\/\(.*\)$' -> '/src/store/\1' 43 | 44 | [strict] 45 | -------------------------------------------------------------------------------- /src/client/themes/global.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | 3 | html { 4 | --primary__background: #{$ghost__white}; 5 | 6 | --primary__text: #{$celestial__blue}; 7 | 8 | --secondary__text: #{$arsenic}; 9 | } 10 | 11 | html[theme='dark'] { 12 | --primary__background: #{$raisin__black}; 13 | 14 | --primary__text: #{$celestial__blue}; 15 | 16 | --secondary__text: #{$silver__sand}; 17 | } 18 | 19 | body { 20 | background: var(--primary__background); 21 | 22 | color: var(--secondary__text); 23 | 24 | .btn-primary { 25 | background: #{$celestial__blue}; 26 | 27 | border-color: #{$celestial__blue}; 28 | 29 | outline: none; 30 | 31 | &:hover { 32 | background: #{$steel__blue}; 33 | 34 | border-color: #{$steel__blue}; 35 | } 36 | 37 | &:focus, 38 | &:not(:disabled):not(.disabled):active, 39 | &:not(:disabled):not(.disabled):active:focus { 40 | background: #{$steel__blue}; 41 | 42 | border-color: #{$steel__blue}; 43 | 44 | box-shadow: 0 0 0 0.2rem #{$steel__blue}; 45 | } 46 | } 47 | 48 | a:hover { 49 | color: #{$steel__blue}; 50 | } 51 | 52 | img { 53 | max-width: 100%; 54 | } 55 | 56 | .mde-preview { 57 | .mde-preview-content { 58 | a { 59 | margin: 0 0.2rem; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist/ 64 | 65 | stats.json 66 | 67 | __snapshots__/ 68 | 69 | .vscode/settings.json 70 | 71 | public/* 72 | !public/assets 73 | !public/locales 74 | !public/robots.txt 75 | !public/googledb37d62693032295.html -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { createBrowserHistory, createMemoryHistory } from 'history'; 3 | import { createStore, applyMiddleware } from 'redux'; 4 | import { routerMiddleware } from 'connected-react-router'; 5 | import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; 6 | import thunk from 'redux-thunk'; 7 | import { type ConfigureStoreType } from 'types'; 8 | import createReducers from './reducer'; 9 | 10 | const configureStore = ({ initialState, url }: ConfigureStoreType) => { 11 | const isServer = typeof window === 'undefined'; 12 | 13 | const history = isServer 14 | ? createMemoryHistory({ initialEntries: [url || '/'] }) 15 | : createBrowserHistory(); 16 | 17 | const middlewares = [routerMiddleware(history), thunk]; 18 | 19 | const enhancers = composeWithDevTools(applyMiddleware(...middlewares)); 20 | 21 | const store = createStore( 22 | createReducers(history), 23 | initialState || {}, 24 | enhancers, 25 | ); 26 | 27 | if (module.hot) { 28 | module.hot.accept('./reducer', async () => { 29 | try { 30 | const { default: nextReducer } = await import('./reducer'); 31 | 32 | store.replaceReducer(nextReducer(history)); 33 | } catch (error) { 34 | console.error(`==> 😭 Reducer hot reloading error ${error}`); 35 | } 36 | }); 37 | } 38 | 39 | return { 40 | store, 41 | history, 42 | }; 43 | }; 44 | 45 | export default configureStore; 46 | -------------------------------------------------------------------------------- /src/client/components/TagsInput/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const TagsInput = ({ 5 | className, 6 | inputClassName, 7 | placeholder = 'Enter your tag', 8 | value = [], 9 | tagComponent, 10 | onChange, 11 | }) => { 12 | const [tags, setTags] = useState(value); 13 | 14 | const [val, setVal] = useState(''); 15 | 16 | useEffect(() => { 17 | setTags(value); 18 | }, [value]); 19 | 20 | const onInputKeyDown = async ({ keyCode, target }) => { 21 | if (keyCode === 13 && target.value) { 22 | const newTags = [...tags, target.value]; 23 | 24 | setTags(newTags); 25 | 26 | setVal(''); 27 | 28 | onChange(newTags); 29 | } 30 | }; 31 | 32 | const onInputChange = ({ target }) => setVal(target.value); 33 | 34 | return ( 35 |
    36 | {tags.map((tag, i) => tagComponent(tag, i))} 37 | 38 | 45 |
46 | ); 47 | }; 48 | 49 | TagsInput.propTypes = { 50 | className: PropTypes.string, 51 | inputClassName: PropTypes.string, 52 | placeholder: PropTypes.string, 53 | value: PropTypes.array, 54 | tagComponent: PropTypes.func, 55 | onChange: PropTypes.func.isRequired, 56 | }; 57 | 58 | export default TagsInput; 59 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest/globals": true 7 | }, 8 | "extends": [ 9 | "airbnb-base", 10 | "prettier", 11 | "plugin:flowtype/recommended", 12 | "plugin:react/recommended", 13 | "plugin:import/recommended", 14 | "plugin:jest/recommended" 15 | ], 16 | "globals": { 17 | "Atomics": "readonly", 18 | "SharedArrayBuffer": "readonly", 19 | "__DEV__": true, 20 | "__SERVER__": false, 21 | "__CLIENT__": false 22 | }, 23 | "parserOptions": { 24 | "ecmaFeatures": { 25 | "jsx": true 26 | }, 27 | "ecmaVersion": 2018, 28 | "sourceType": "module" 29 | }, 30 | "plugins": ["flowtype", "react", "import", "jest"], 31 | "settings": { 32 | "import/resolver": { 33 | "babel-module": {} 34 | }, 35 | "react": { 36 | "pragma": "React", 37 | "version": "detect", 38 | "flowVersion": "detect" 39 | } 40 | }, 41 | "rules": { 42 | "global-require": "off", 43 | "import/no-extraneous-dependencies": [ 44 | "error", 45 | { 46 | "devDependencies": true, 47 | "optionalDependencies": false, 48 | "peerDependencies": false 49 | } 50 | ], 51 | "react/prop-types": "warn", 52 | "no-underscore-dangle": "warn", 53 | "import/prefer-default-export": "off", 54 | "import/no-cycle": "off", 55 | "import/no-unresolved": "off", 56 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/models/result.model.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type ResultModelType, type ResultModelErrorType } from 'types'; 3 | 4 | export const resultModel = ({ 5 | code = 200, 6 | data, 7 | error, 8 | }: ResultModelType): ResultModelType => ({ 9 | code, 10 | data, 11 | error, 12 | }); 13 | 14 | export const success = (): ResultModelType => resultModel({ data: 'Success.' }); 15 | 16 | export const badRequest = ({ 17 | message = 'Bad request.', 18 | extras, 19 | }: ResultModelErrorType = {}): ResultModelType => 20 | resultModel({ 21 | code: 400, 22 | error: { message, ...extras }, 23 | }); 24 | 25 | export const unauthorized = ({ 26 | message = 'Unauthorized.', 27 | extras, 28 | }: ResultModelErrorType = {}): ResultModelType => 29 | resultModel({ 30 | code: 401, 31 | error: { message, ...extras }, 32 | }); 33 | 34 | export const notFoundError = ({ 35 | message = 'Not found.', 36 | extras, 37 | }: ResultModelErrorType = {}): ResultModelType => 38 | resultModel({ 39 | code: 404, 40 | error: { message, ...extras }, 41 | }); 42 | 43 | export const internalServerError = ({ 44 | message = 'Internal server error.', 45 | extras, 46 | }: ResultModelErrorType = {}) => 47 | resultModel({ 48 | code: 500, 49 | error: { message, ...extras }, 50 | }); 51 | 52 | export const genericError = ({ 53 | message = 'Something went wrong.', 54 | extras, 55 | }: ResultModelErrorType = {}) => 56 | resultModel({ 57 | code: 1000, 58 | error: { message, ...extras }, 59 | }); 60 | -------------------------------------------------------------------------------- /src/client/components/MdViewer/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ReactHTMLParser from 'react-html-parser'; 4 | import { toArray } from 'react-emoji-render'; 5 | import { Converter } from 'showdown'; 6 | 7 | const converter = new Converter({ 8 | omitExtraWLInCodeBlocks: true, 9 | noHeaderId: false, 10 | parseImgDimensions: true, 11 | simplifiedAutoLink: true, 12 | literalMidWordUnderscores: true, 13 | strikethrough: true, 14 | tables: true, 15 | tablesHeaderId: false, 16 | ghCodeBlocks: true, 17 | tasklists: true, 18 | smoothLivePreview: true, 19 | prefixHeaderId: false, 20 | disableForced4SpacesIndentedSublists: false, 21 | ghCompatibleHeaderId: true, 22 | smartIndentationFix: false, 23 | }); 24 | 25 | converter.setFlavor('github'); 26 | 27 | const parseEmojis = (value) => { 28 | const emojisArray = toArray(value); 29 | 30 | const newValue = emojisArray.reduce((previous, current) => { 31 | if (typeof current === 'string') { 32 | return previous + current; 33 | } 34 | return previous + current.props.children; 35 | }, ''); 36 | 37 | return newValue; 38 | }; 39 | 40 | const makeEmojiHtml = (source) => parseEmojis(converter.makeHtml(source)); 41 | 42 | const MdViewer = ({ source = '' }) => { 43 | return ( 44 |
45 |
46 | <>{ReactHTMLParser(makeEmojiHtml(source))} 47 |
48 |
49 | ); 50 | }; 51 | 52 | MdViewer.propTypes = { 53 | source: PropTypes.string, 54 | }; 55 | 56 | export { converter, makeEmojiHtml }; 57 | 58 | export default MdViewer; 59 | -------------------------------------------------------------------------------- /src/client/components/Layout/styles.scss: -------------------------------------------------------------------------------- 1 | .main__container { 2 | margin-top: 24px; 3 | margin-bottom: 24px; 4 | 5 | .sidebar { 6 | display: table; 7 | 8 | &__section { 9 | margin: 24px 0; 10 | 11 | h5 { 12 | margin: 0; 13 | } 14 | } 15 | 16 | &__title { 17 | &__container { 18 | display: flex; 19 | align-items: center; 20 | } 21 | 22 | color: var(--primary__text); 23 | } 24 | 25 | &__item { 26 | padding: 0.5rem 0; 27 | color: var(--secondary__text); 28 | 29 | &:hover { 30 | color: var(--primary__text); 31 | } 32 | 33 | &.active { 34 | text-decoration: underline; 35 | } 36 | } 37 | 38 | &__link { 39 | width: 36px; 40 | height: 36px; 41 | display: flex; 42 | justify-content: center; 43 | align-items: center; 44 | background: var(--primary__background); 45 | border-color: var(--secondary__text); 46 | color: var(--secondary__text); 47 | margin: 0.5rem 0.5rem 0 0 !important; 48 | 49 | &:hover { 50 | border-color: var(--primary__text); 51 | color: var(--primary__text); 52 | } 53 | } 54 | } 55 | } 56 | 57 | @media (min-width: 992px) { 58 | .main__container { 59 | .sidebar { 60 | border-right: 2px solid var(--primary__text); 61 | 62 | position: sticky; 63 | 64 | top: 24px; 65 | } 66 | } 67 | } 68 | 69 | .switch__icon { 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | height: 100%; 74 | padding-right: 2px; 75 | 76 | .fa-sun { 77 | color: orange; 78 | } 79 | 80 | .fa-moon { 81 | color: yellow; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/store/reducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type ActionType, type GlobalStateType } from 'types'; 3 | import { combineReducers } from 'redux'; 4 | import { type History } from 'history'; 5 | import { reducer as form } from 'redux-form'; 6 | import { connectRouter } from 'connected-react-router'; 7 | import login from 'pages/Login/reducer'; 8 | import register from 'pages/Register/reducer'; 9 | import postReducer from 'pages/Post/reducer'; 10 | import { UPDATE_TOKEN, UPDATE_LOADING, UPDATE_THEME, GET_ME } from './action'; 11 | 12 | const initialState: GlobalStateType = { 13 | isLoading: false, 14 | accessToken: null, 15 | refreshToken: null, 16 | user: null, 17 | theme: 'light', 18 | }; 19 | 20 | const global = (state: any = initialState, action: ActionType) => { 21 | switch (action.type) { 22 | case UPDATE_TOKEN: { 23 | let s = { 24 | ...state, 25 | ...action.payload, 26 | }; 27 | 28 | if (!s.accessToken) { 29 | s = { ...s, user: null }; 30 | } 31 | 32 | return s; 33 | } 34 | case UPDATE_LOADING: { 35 | return { ...state, isLoading: action.payload }; 36 | } 37 | case UPDATE_THEME: { 38 | return { ...state, theme: action.payload }; 39 | } 40 | case GET_ME.SUCCESS: { 41 | return { ...state, user: action.payload }; 42 | } 43 | case GET_ME.ERROR: { 44 | return { ...state, user: null }; 45 | } 46 | default: 47 | return { ...state }; 48 | } 49 | }; 50 | 51 | const createReducers = (history: History) => 52 | combineReducers({ 53 | router: connectRouter(history), 54 | form, 55 | global, 56 | login, 57 | register, 58 | postReducer, 59 | }); 60 | 61 | export default createReducers; 62 | -------------------------------------------------------------------------------- /src/client/pages/Introduce/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import chunk from 'lodash/chunk'; 4 | import take from 'lodash/take'; 5 | 6 | import Layout from 'components/Layout'; 7 | import MdViewer from 'components/MdViewer'; 8 | 9 | import './styles.scss'; 10 | 11 | import { profile, projects } from './profile'; 12 | 13 | const newest = take(projects, 3); 14 | 15 | if (projects.length > 3) newest.push(null); 16 | 17 | const projectsGrouped = chunk(newest, 2); 18 | 19 | const Introduce = ({ route: { title } }) => { 20 | return ( 21 | <> 22 | 23 | 24 | 25 | 26 | 27 | {projectsGrouped.map((group, i) => { 28 | return ( 29 |
30 | {group.map((item, j) => { 31 | if (!item) { 32 | return ( 33 | 37 |
38 | 39 |

More

40 |
41 | 42 | ); 43 | } 44 | 45 | return ( 46 |
47 | 48 |
49 | ); 50 | })} 51 |
52 | ); 53 | })} 54 |
55 | 56 | ); 57 | }; 58 | 59 | export default Introduce; 60 | -------------------------------------------------------------------------------- /src/api/comment/controller.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response } from 'express'; 3 | import { ObjectId } from 'mongodb'; 4 | import head from 'lodash/head'; 5 | import { genericError, resultModel } from 'models/result.model'; 6 | 7 | export const getCommentsController = () => async ( 8 | req: Request, 9 | res: Response, 10 | ) => { 11 | const { 12 | params: { _id }, 13 | commentsCollection, 14 | } = req; 15 | 16 | try { 17 | const comments = await commentsCollection 18 | .aggregate([ 19 | { $match: { post_id: ObjectId(_id) } }, 20 | { 21 | $lookup: { 22 | let: { user: '$user_id' }, 23 | from: 'users', 24 | pipeline: [ 25 | { $match: { $expr: { $eq: ['$$user', '$_id'] } } }, 26 | { $project: { _id: true, name: true } }, 27 | ], 28 | as: 'user', 29 | }, 30 | }, 31 | { $unwind: '$user' }, 32 | ]) 33 | .toArray(); 34 | 35 | return res.json(resultModel({ data: comments })); 36 | } catch (error) { 37 | return res.json(genericError({ message: error.message })); 38 | } 39 | }; 40 | 41 | export const postCommentController = () => async ( 42 | req: Request, 43 | res: Response, 44 | ) => { 45 | const { 46 | body: { _id, comment }, 47 | user, 48 | commentsCollection, 49 | } = req; 50 | 51 | try { 52 | const { ops: data } = await commentsCollection.insertOne( 53 | { 54 | post_id: ObjectId(_id), 55 | user_id: ObjectId(user._id), 56 | comment, 57 | createAt: new Date(), 58 | }, 59 | { serializeFunctions: true }, 60 | ); 61 | 62 | return res.json(resultModel({ data: head(data) })); 63 | } catch (error) { 64 | return res.json(genericError({ message: error.message })); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import './vendor'; 3 | import '../i18n'; 4 | import React, { Suspense } from 'react'; 5 | import { AppContainer } from 'react-hot-loader'; 6 | import { render, hydrate } from 'react-dom'; 7 | import { renderRoutes } from 'react-router-config'; 8 | import { CookiesProvider } from 'react-cookie'; 9 | import { Provider } from 'react-redux'; 10 | import { ConnectedRouter } from 'connected-react-router'; 11 | import { LastLocationProvider } from 'react-router-last-location'; 12 | import { loadableReady } from '@loadable/component'; 13 | import Loading from 'components/Loading'; 14 | import configureStore from '../store'; 15 | import routes from '../routes'; 16 | 17 | const initialState = window.__INITIAL_STATE__; 18 | 19 | delete window.__INITIAL_STATE__; 20 | 21 | const { store, history } = configureStore({ initialState }); 22 | 23 | const bootstrap = (routesConfig: Array) => { 24 | const renderMethod = module.hot ? render : hydrate; 25 | 26 | renderMethod( 27 | }> 28 | 29 | 30 | 31 | 32 | {renderRoutes(routesConfig)} 33 | 34 | 35 | 36 | 37 | , 38 | document.getElementById('react-view'), 39 | ); 40 | }; 41 | 42 | loadableReady(() => { 43 | bootstrap(routes); 44 | }); 45 | 46 | if (module.hot) { 47 | module.hot.accept('../routes', async () => { 48 | try { 49 | const nextRoutes = await import('../routes'); 50 | 51 | bootstrap(nextRoutes.default); 52 | } catch (error) { 53 | console.error(`==> 😭 Routes hot reloading error ${error}`); 54 | } 55 | }); 56 | } 57 | 58 | if (!__DEV__) { 59 | require('offline-plugin/runtime').install(); 60 | } 61 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import Login from 'pages/Login'; 2 | import Register from 'pages/Register'; 3 | import Post from 'pages/Post'; 4 | import { getPostsAction } from 'pages/Post/action'; 5 | import PostDetail from 'pages/Post/PostDetail'; 6 | import { 7 | getPostDetailAction, 8 | getCommentsAction, 9 | } from 'pages/Post/PostDetail/action'; 10 | import CreatePost from 'pages/Post/CreatePost'; 11 | import Introduce from 'pages/Introduce'; 12 | import Projects from 'pages/Introduce/Projects'; 13 | import Contact from 'pages/Contact'; 14 | import NotFound from 'pages/NotFound'; 15 | import App from './client/app'; 16 | 17 | export default [ 18 | { 19 | component: App, 20 | routes: [ 21 | { 22 | path: '/', 23 | exact: true, 24 | component: Post, 25 | title: 'Post', 26 | loadData: ({ _params }) => [getPostsAction()], 27 | }, 28 | { 29 | path: '/p/:_id', 30 | component: PostDetail, 31 | loadData: ({ params: { _id } }) => [ 32 | getPostDetailAction(_id), 33 | getCommentsAction(_id), 34 | ], 35 | }, 36 | { 37 | path: '/create-post', 38 | component: CreatePost, 39 | title: 'Create post', 40 | }, 41 | { 42 | path: '/login', 43 | component: Login, 44 | title: 'Login', 45 | }, 46 | { 47 | path: '/register', 48 | component: Register, 49 | title: 'Register', 50 | }, 51 | { 52 | path: '/introduce/projects', 53 | component: Projects, 54 | title: 'Projects', 55 | }, 56 | { 57 | path: '/introduce', 58 | component: Introduce, 59 | title: 'Introduce', 60 | }, 61 | { 62 | path: '/contact', 63 | component: Contact, 64 | title: 'Contact', 65 | }, 66 | { 67 | component: NotFound, 68 | title: 'Error', 69 | }, 70 | ], 71 | }, 72 | ]; 73 | -------------------------------------------------------------------------------- /src/client/pages/Post/PostDetail/action.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Dispatch } from 'redux'; 3 | import { type PostCommentActionType, type ApiDataType } from 'types'; 4 | import { requestAction } from 'utils/request'; 5 | import { actionGenerator } from 'utils'; 6 | 7 | export const GET_POST_DETAIL = actionGenerator('@@GET_POST_DETAIL'); 8 | 9 | export const getPostDetailAction = (_id: string) => (dispatch: Dispatch) => 10 | dispatch( 11 | requestAction({ 12 | url: `/post/detail/${_id}`, 13 | label: GET_POST_DETAIL.NAME, 14 | onSuccess: ({ data }: ApiDataType) => { 15 | dispatch({ type: GET_POST_DETAIL.SUCCESS, payload: data }); 16 | }, 17 | onError: ({ error }: ApiDataType) => { 18 | dispatch({ type: GET_POST_DETAIL.ERROR, payload: error }); 19 | }, 20 | }), 21 | ); 22 | 23 | export const GET_COMMENTS = actionGenerator('@@GET_COMMENTS'); 24 | 25 | export const getCommentsAction = (_id: string) => (dispatch: Dispatch) => 26 | dispatch( 27 | requestAction({ 28 | url: `/comment/get-comments/${_id}`, 29 | label: GET_COMMENTS.NAME, 30 | onSuccess: ({ data }: ApiDataType) => { 31 | dispatch({ type: GET_COMMENTS.SUCCESS, payload: data }); 32 | }, 33 | onError: ({ error }: ApiDataType) => { 34 | dispatch({ type: GET_COMMENTS.ERROR, payload: error }); 35 | }, 36 | }), 37 | ); 38 | 39 | export const POST_COMMENT = actionGenerator('@@POST_COMMENT'); 40 | 41 | export const postCommentAction = (data: PostCommentActionType) => ( 42 | dispatch: Dispatch, 43 | ) => 44 | dispatch( 45 | requestAction({ 46 | url: '/comment/post-comment', 47 | label: POST_COMMENT.NAME, 48 | data, 49 | method: 'POST', 50 | onSuccess: ({ data: res }: ApiDataType) => { 51 | dispatch({ type: POST_COMMENT.SUCCESS, payload: res }); 52 | }, 53 | onError: ({ error }: ApiDataType) => { 54 | dispatch({ type: POST_COMMENT.ERROR, payload: error }); 55 | }, 56 | }), 57 | ); 58 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const commander = require('commander'); 4 | const gitClone = require('git-clone'); 5 | const del = require('del'); 6 | const { writeFileSync } = require('fs'); 7 | const { omit } = require('lodash'); 8 | const packageJson = require('../package.json'); 9 | 10 | const TEMPLATE_DIR = 'https://github.com/htdangkhoa/erb.git'; 11 | 12 | const NAME = packageJson.name; 13 | 14 | commander 15 | .name('erb-gen') 16 | .version(packageJson.version, '-v, --version') 17 | .option('-d, --dir ', `project's directory.`, NAME) 18 | .option('-n, --name ', `project's name.`, NAME) 19 | .parse(process.argv); 20 | 21 | const main = () => { 22 | const { dir, name } = commander.opts(); 23 | 24 | console.info(`Initializing project ${name}...`); 25 | 26 | gitClone(TEMPLATE_DIR, dir, (error) => { 27 | if (error) { 28 | const dirExisted = 29 | error.message.replace(`'git clone' failed with status`, '').trim() === 30 | '128'; 31 | 32 | if (dirExisted) { 33 | console.error(new Error('directory already exists.')); 34 | } else { 35 | console.error(error); 36 | } 37 | 38 | process.exit(1); 39 | 40 | return; 41 | } 42 | 43 | del.sync( 44 | [ 45 | `${dir}/bin`, 46 | `${dir}/public/googledb37d62693032295.html`, 47 | `${dir}/static.json`, 48 | `${dir}/.git/`, 49 | `${dir}/yarn.lock`, 50 | `${dir}/.github/funding.yml`, 51 | ], 52 | { force: true }, 53 | ); 54 | 55 | const newPackage = omit(packageJson, [ 56 | 'author', 57 | 'contributors', 58 | 'homepage', 59 | 'bugs', 60 | 'repository', 61 | 'keywords', 62 | 'bin', 63 | ]); 64 | 65 | Object.assign(newPackage, { 66 | version: '1.0.0', 67 | name, 68 | description: '', 69 | }); 70 | 71 | writeFileSync( 72 | `${dir}/package.json`, 73 | JSON.stringify(newPackage, null, 2), 74 | 'utf8', 75 | ); 76 | 77 | console.info('Done!!!'); 78 | }); 79 | }; 80 | 81 | main(); 82 | -------------------------------------------------------------------------------- /src/client/pages/Login/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Field, reduxForm } from 'redux-form'; 4 | // import { isEmail } from 'validator'; 5 | 6 | import Layout from 'components/Layout'; 7 | 8 | import * as action from './action'; 9 | 10 | let Login = ({ handleSubmit, loginAction, route: { title } }) => { 11 | const onSubmit = async (value) => { 12 | loginAction(value); 13 | }; 14 | 15 | return ( 16 | 17 |

Login

18 | 19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | 34 |
35 | 36 |
37 | 38 | 39 | 40 | 48 |
49 | 50 | 53 |
54 |
55 |
56 |
57 | ); 58 | }; 59 | 60 | Login = reduxForm({ 61 | form: 'Login', 62 | })(Login); 63 | 64 | const mapStateToProps = ({ global, login }) => ({ 65 | global, 66 | login, 67 | }); 68 | 69 | const mapDispatchToProps = { 70 | loginAction: action.loginAction, 71 | }; 72 | 73 | export default connect(mapStateToProps, mapDispatchToProps)(Login); 74 | -------------------------------------------------------------------------------- /src/client/pages/Post/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import Paginate from 'react-paginate'; 5 | 6 | import Layout from 'components/Layout'; 7 | 8 | // import image from 'assets/image.png'; 9 | import { formatDate } from 'utils'; 10 | import * as action from './action'; 11 | import './styles.scss'; 12 | 13 | const Post = ({ 14 | route: { title }, 15 | post: { 16 | posts, 17 | metaData: { index: page, total }, 18 | }, 19 | getPostsAction, 20 | }) => { 21 | useEffect(() => { 22 | if (!posts || posts.length === 0) { 23 | getPostsAction(); 24 | } 25 | }, []); 26 | 27 | const onPageChange = ({ selected: skip }) => { 28 | getPostsAction(skip); 29 | }; 30 | 31 | return ( 32 | 33 | {posts.map((post) => ( 34 |
35 |
36 | {`${formatDate(post.publishAt)} - Published by `} 37 | 38 | {post.user?.name} 39 | 40 |
41 | 42 | 43 |

{post.title}

44 | 45 | 46 |

{post.description}

47 | 48 |
49 | {post.tags.map((tag, i) => ( 50 | 51 | {tag} 52 | 53 | ))} 54 |
55 |
56 | ))} 57 | 58 | } 64 | nextLabel={} 65 | onPageChange={onPageChange} 66 | disableInitialCallback 67 | containerClassName={'pagination row'} 68 | subContainerClassName={'pages pagination'} 69 | activeClassName={'active'} 70 | /> 71 |
72 | ); 73 | }; 74 | 75 | const mapStateToProps = ({ postReducer: { post } }) => ({ post }); 76 | 77 | const mapDispatchToProps = { 78 | getPostsAction: action.getPostsAction, 79 | }; 80 | 81 | export default connect(mapStateToProps, mapDispatchToProps)(Post); 82 | -------------------------------------------------------------------------------- /src/types/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Express } from 'express'; 3 | import { type MongoClient, type Db, type Collection } from 'mongodb'; 4 | import { type HelmetData } from 'react-helmet'; 5 | import { type ChunkExtractor } from '@loadable/server'; 6 | 7 | export type MongoConnectionType = { 8 | host: string, 9 | 10 | database: string, 11 | 12 | user?: string, 13 | 14 | password?: string, 15 | 16 | app?: Express, 17 | }; 18 | 19 | export type MongoResultType = { 20 | client: MongoClient, 21 | 22 | db: Db, 23 | }; 24 | 25 | export type ResultModelErrorType = { 26 | message?: string, 27 | 28 | extras?: Object, 29 | }; 30 | 31 | export type ResultModelType = { 32 | code?: number, 33 | 34 | data?: any, 35 | 36 | error?: ResultModelErrorType, 37 | }; 38 | 39 | export type RenderHtmlType = { 40 | head: HelmetData, 41 | 42 | extractor: ChunkExtractor, 43 | 44 | htmlContent: string, 45 | 46 | initialState?: Object, 47 | }; 48 | 49 | export type ConfigureStoreType = { 50 | initialState?: Object, 51 | 52 | url?: string, 53 | }; 54 | 55 | export type ActionType = { 56 | type: string, 57 | 58 | payload: any, 59 | }; 60 | 61 | export type RequestType = { 62 | host?: string, 63 | 64 | url?: string, 65 | 66 | method?: 'GET' | 'POST' | 'PUT' | 'DELETE', 67 | 68 | params?: Object, 69 | 70 | data?: Object, 71 | 72 | headers?: Object, 73 | 74 | token?: string, 75 | }; 76 | 77 | export type ApiDataType = { 78 | code?: number, 79 | 80 | data?: any, 81 | 82 | error?: { message?: string }, 83 | }; 84 | 85 | export type ApiActionType = { 86 | label?: string, 87 | 88 | onSuccess?: (data: ApiDataType) => void | Promise, 89 | 90 | onError?: (data: ApiDataType) => void | Promise, 91 | } & RequestType; 92 | 93 | export type ThemeType = 'light' | 'dark'; 94 | 95 | export type GlobalStateType = { 96 | loading?: boolean, 97 | 98 | accessToken: ?string, 99 | 100 | refreshToken: ?string, 101 | 102 | user?: Object, 103 | 104 | theme?: ThemeType, 105 | }; 106 | 107 | export type PostCommentActionType = { 108 | _id: string, 109 | 110 | comment: string, 111 | }; 112 | 113 | export type MongoPagingType = { 114 | collection: Collection, 115 | 116 | aggregate?: Object[] | any[], 117 | 118 | skip?: number, 119 | 120 | limit?: number, 121 | }; 122 | 123 | export type MongoPagingResultType = { 124 | values: any[], 125 | 126 | metaData: { index: number, total: number }, 127 | }; 128 | -------------------------------------------------------------------------------- /src/client/pages/Post/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'themes/global'; 2 | 3 | .post { 4 | &__item { 5 | margin-bottom: 8px; 6 | 7 | .mde-preview { 8 | background: var(--primary__background); 9 | color: var(--secondary__text); 10 | } 11 | } 12 | 13 | &__title { 14 | color: $celestial__blue; 15 | } 16 | 17 | &__description { 18 | margin-bottom: 12px; 19 | } 20 | } 21 | 22 | .tag { 23 | &__group { 24 | display: flex; 25 | flex-wrap: wrap; 26 | margin-bottom: 24px; 27 | } 28 | 29 | &__item { 30 | margin-right: 0.5rem; 31 | margin-bottom: 0.5rem; 32 | max-width: 120px; 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | border: 1px solid #888; 36 | border-radius: 4px; 37 | padding: 0 4px; 38 | background: #eee; 39 | color: #888; 40 | } 41 | } 42 | 43 | .comment { 44 | &__container { 45 | display: block; 46 | } 47 | 48 | &__login { 49 | margin-bottom: 12px; 50 | background: var(--primary__background); 51 | border-color: var(--secondary__text); 52 | 53 | a { 54 | color: var(--secondary__text); 55 | 56 | &:hover { 57 | color: var(--primary__text); 58 | } 59 | } 60 | } 61 | 62 | &__submit { 63 | margin: 24px 0; 64 | } 65 | 66 | &__item { 67 | margin-bottom: 12px; 68 | background: var(--primary__background); 69 | border-color: var(--secondary__text); 70 | 71 | .mde-preview { 72 | background: var(--primary__background); 73 | color: var(--secondary__text); 74 | } 75 | } 76 | } 77 | 78 | .mde-text { 79 | color: #495057; 80 | } 81 | 82 | .pagination { 83 | margin: 8px 0; 84 | justify-content: center; 85 | 86 | li { 87 | border: 2px solid var(--secondary__text); 88 | border-radius: 4px; 89 | margin: 4px; 90 | font-size: larger; 91 | min-width: 38px; 92 | min-height: 38px; 93 | line-height: 19px; 94 | justify-content: center; 95 | display: flex; 96 | 97 | a { 98 | color: var(--secondary__text); 99 | cursor: pointer; 100 | padding: 8px; 101 | 102 | &:focus { 103 | outline: none; 104 | } 105 | } 106 | 107 | &.active { 108 | border: 2px solid var(--primary__text); 109 | 110 | a { 111 | color: var(--primary__text); 112 | } 113 | } 114 | 115 | &.break { 116 | pointer-events: none; 117 | cursor: none; 118 | border-color: var(--secondary__text); 119 | 120 | a { 121 | color: var(--secondary__text); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/utils/render-html.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import serialize from 'serialize-javascript'; 3 | import { minify } from 'html-minifier'; 4 | import { isDev } from '../config'; 5 | import { type RenderHtmlType } from '../types'; 6 | 7 | const renderHtml = ({ 8 | head, 9 | extractor, 10 | htmlContent, 11 | initialState = {}, 12 | }: RenderHtmlType) => { 13 | const html = ` 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ${ 31 | !isDev 32 | ? ` 33 | 37 | 38 | `.trim() 39 | : '' 40 | } 41 | 42 | ${head.title.toString()} 43 | ${head.base.toString()} 44 | ${head.meta.toString()} 45 | ${head.link.toString()} 46 | 47 | 48 | ${extractor.getLinkTags()} 49 | ${extractor.getStyleTags()} 50 | 51 | 52 | 53 |
${htmlContent}
54 | 55 | 56 | 57 | 58 | ${extractor.getScriptTags()} 59 | 60 | ${head.script.toString()} 61 | 62 | 63 | `; 64 | 65 | const minifyOptions = { 66 | collapseWhitespace: true, 67 | removeComments: true, 68 | trimCustomFragments: true, 69 | minifyCSS: true, 70 | minifyJS: true, 71 | minifyURLs: true, 72 | }; 73 | 74 | return isDev ? html : minify(html, minifyOptions); 75 | }; 76 | 77 | export default renderHtml; 78 | -------------------------------------------------------------------------------- /src/client/pages/Register/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RedirectWithoutLastLocation } from 'react-router-last-location'; 3 | import { connect } from 'react-redux'; 4 | import { Field, reduxForm } from 'redux-form'; 5 | 6 | import Layout from 'components/Layout'; 7 | 8 | import * as action from './action'; 9 | 10 | let Register = ({ 11 | route: { title }, 12 | handleSubmit, 13 | register: { registerSuccess }, 14 | registerAction, 15 | }) => { 16 | const onSubmit = (value) => { 17 | registerAction(value); 18 | }; 19 | 20 | return registerSuccess ? ( 21 | 22 | ) : ( 23 | 24 |

Register

25 | 26 |
27 |
28 |
29 |
30 | 31 | 32 | 33 | 40 |
41 | 42 |
43 | 44 | 45 | 46 | 54 |
55 | 56 |
57 | 58 | 59 | 60 | 68 |
69 | 70 | 73 |
74 |
75 |
76 |
77 | ); 78 | }; 79 | 80 | Register = reduxForm({ 81 | form: 'Register', 82 | })(Register); 83 | 84 | const mapStateToProps = ({ global, register }) => ({ global, register }); 85 | 86 | const mapDispatchToProps = { 87 | registerAction: action.registerAction, 88 | }; 89 | 90 | export default connect(mapStateToProps, mapDispatchToProps)(Register); 91 | -------------------------------------------------------------------------------- /src/api/post/controller.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response } from 'express'; 3 | import { ObjectId } from 'mongodb'; 4 | import { usePaging } from 'mongo/helper'; 5 | import { resultModel, genericError, badRequest } from 'models/result.model'; 6 | import head from 'lodash/head'; 7 | import compact from 'lodash/compact'; 8 | 9 | const aggregateLookupUser = [ 10 | { 11 | $lookup: { 12 | let: { user: '$user_id' }, 13 | from: 'users', 14 | pipeline: [ 15 | { $match: { $expr: { $eq: ['$$user', '$_id'] } } }, 16 | { $project: { _id: true, name: true } }, 17 | ], 18 | as: 'user', 19 | }, 20 | }, 21 | { $unwind: '$user' }, 22 | ]; 23 | 24 | export const getPostsController = () => async (req: Request, res: Response) => { 25 | const { 26 | postsCollection, 27 | query: { skip = 0 }, 28 | } = req; 29 | 30 | try { 31 | const { values: posts, metaData } = await usePaging({ 32 | collection: postsCollection, 33 | aggregate: [...aggregateLookupUser, { $sort: { publishAt: -1 } }], 34 | skip, 35 | }); 36 | 37 | return res.json( 38 | resultModel({ 39 | data: { posts, metaData }, 40 | }), 41 | ); 42 | } catch (error) { 43 | return res.json(genericError({ message: error.message })); 44 | } 45 | }; 46 | 47 | export const getPostDetailController = () => async ( 48 | req: Request, 49 | res: Response, 50 | ) => { 51 | const { 52 | params: { _id }, 53 | postsCollection, 54 | } = req; 55 | 56 | if (!_id || !ObjectId.isValid(_id)) { 57 | return res.json(badRequest()); 58 | } 59 | 60 | try { 61 | const posts = await postsCollection 62 | .aggregate([{ $match: { _id: ObjectId(_id) } }, ...aggregateLookupUser]) 63 | .toArray(); 64 | 65 | return res.json(resultModel({ data: head(posts) })); 66 | } catch (error) { 67 | return res.json(genericError({ message: error.message })); 68 | } 69 | }; 70 | 71 | export const createPostController = () => async ( 72 | req: Request, 73 | res: Response, 74 | ) => { 75 | const { 76 | body: { title, description, content, tags = '' }, 77 | user, 78 | postsCollection, 79 | } = req; 80 | 81 | const listTag = compact(tags.split(',').map((tag) => tag.trim())); 82 | 83 | if (!title || !description || !content || listTag.length === 0) { 84 | return res.json(badRequest()); 85 | } 86 | 87 | try { 88 | const { ops } = await postsCollection.insertOne( 89 | { 90 | title, 91 | description, 92 | content, 93 | tags: listTag, 94 | comments: [], 95 | viewers: [], 96 | publishAt: new Date(), 97 | user_id: user._id, 98 | }, 99 | { serializeFunction: true }, 100 | ); 101 | 102 | return res.json(resultModel({ data: head(ops) })); 103 | } catch (error) { 104 | return res.json(genericError({ message: error.message })); 105 | } 106 | }; 107 | -------------------------------------------------------------------------------- /src/api/auth/controller.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type Request, type Response } from 'express'; 3 | import { genSaltSync, hashSync, compareSync } from 'bcrypt'; 4 | import head from 'lodash/head'; 5 | import { 6 | badRequest, 7 | resultModel, 8 | genericError, 9 | unauthorized, 10 | } from 'models/result.model'; 11 | import { sign, verify, TYPE_ACCESS, TYPE_REFRESH } from '../../secure/jwt'; 12 | 13 | const hashPassword = (password: string): string => 14 | hashSync(password, genSaltSync()); 15 | 16 | const generateToken = (userId: string) => ({ 17 | accessToken: sign({ _id: userId, type: TYPE_ACCESS }), 18 | refreshToken: sign({ _id: userId, type: TYPE_REFRESH }, 604800), // 7 days. 19 | }); 20 | 21 | export const registerController = () => async (req: Request, res: Response) => { 22 | const { 23 | body: { email, password, name }, 24 | usersCollection, 25 | } = req; 26 | 27 | if (!email || !password || !name) { 28 | return res.json(badRequest()); 29 | } 30 | 31 | try { 32 | const user = await usersCollection.findOne({ email }); 33 | 34 | if (user) { 35 | return res.json(unauthorized({ message: 'User already exist.' })); 36 | } 37 | 38 | const { ops: data } = await usersCollection.insertOne( 39 | { email, password: hashPassword(password), name }, 40 | { serializeFunctions: true }, 41 | ); 42 | 43 | return res.json(resultModel({ data: head(data) })); 44 | } catch (error) { 45 | return res.json(genericError({ message: error.message })); 46 | } 47 | }; 48 | 49 | export const loginController = () => async (req: Request, res: Response) => { 50 | const { 51 | body: { email, password }, 52 | usersCollection, 53 | } = req; 54 | 55 | if (!email || !password) { 56 | return res.json(badRequest()); 57 | } 58 | 59 | try { 60 | const user = await usersCollection.findOne({ email }); 61 | 62 | if (!user) { 63 | return res.json(genericError({ message: 'User not found.' })); 64 | } 65 | 66 | if (!compareSync(password, user.password)) { 67 | return res.json(genericError({ message: 'Password does not match.' })); 68 | } 69 | 70 | const data = generateToken(user._id); 71 | 72 | return res.json(resultModel({ data })); 73 | } catch (error) { 74 | return res.json(genericError({ message: error.message })); 75 | } 76 | }; 77 | 78 | export const renewTokenController = () => async ( 79 | req: Request, 80 | res: Response, 81 | ) => { 82 | const { 83 | body: { refreshToken }, 84 | } = req; 85 | 86 | if (!refreshToken) { 87 | return res.json(badRequest()); 88 | } 89 | 90 | try { 91 | const payload = verify(refreshToken); 92 | 93 | if (!payload || payload?.type !== TYPE_REFRESH) { 94 | return res.json(unauthorized()); 95 | } 96 | 97 | const newToken = generateToken(payload._id); 98 | 99 | return res.json(resultModel({ data: newToken })); 100 | } catch (error) { 101 | return res.json(genericError({ message: error.message })); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import axios, { type AxiosError } from 'axios'; 3 | import omit from 'lodash/omit'; 4 | import { type Dispatch } from 'redux'; 5 | import { type RequestType, type ApiActionType, type ApiDataType } from 'types'; 6 | import { updateLoadingAction } from 'store/action'; 7 | import cookies from './cookies'; 8 | import { actionGenerator } from './'; 9 | 10 | const getBaseUrl = () => { 11 | if (!__DEV__) return 'https://htdangkhoa-erb.herokuapp.com/api'; 12 | 13 | if (typeof window !== 'undefined') { 14 | return `${window.location.protocol}//${window.location.host}/api`; 15 | } 16 | 17 | return `http://localhost:${String(process.env.PORT)}/api`; 18 | }; 19 | 20 | const baseUrl = getBaseUrl(); 21 | 22 | export const request = async ({ 23 | host = baseUrl, 24 | url = '', 25 | method, 26 | params = {}, 27 | data = {}, 28 | headers = {}, 29 | token, 30 | }: RequestType) => { 31 | const authorization = token || cookies.get('accessToken'); 32 | 33 | let config = { 34 | method, 35 | params, 36 | data, 37 | headers: { 38 | 'Access-Control-Allow-Origin': '*', 39 | 'Content-Type': 'application/json', 40 | Authorization: `Bearer ${authorization}`, 41 | ...headers, 42 | }, 43 | }; 44 | 45 | if (method === 'GET' || method === 'DELETE') { 46 | config = omit(config, ['data']); 47 | } 48 | 49 | const result = await axios({ 50 | url: `${host}${url}`, 51 | ...config, 52 | }); 53 | 54 | return result; 55 | }; 56 | 57 | export const requestAction = (options: ApiActionType) => async ( 58 | dispatch: Dispatch, 59 | ) => { 60 | if (__CLIENT__) { 61 | dispatch(updateLoadingAction(true)); 62 | } 63 | 64 | const opt = omit(options, ['onSuccess', 'onError']); 65 | 66 | const requestOptions: RequestType = (opt: RequestType); 67 | 68 | const ACTION = actionGenerator(`${options.label || 'REQUEST_API_ACTION'}`); 69 | 70 | try { 71 | const { data: res } = await request(requestOptions); 72 | 73 | if (__CLIENT__) { 74 | dispatch(updateLoadingAction(false)); 75 | } 76 | 77 | const result: ApiDataType = (res: ApiDataType); 78 | 79 | // eslint-disable-next-line no-unused-vars 80 | const { code = 200, data, error } = result; 81 | 82 | if (code !== 200 && options.onError) { 83 | return options.onError(result); 84 | } 85 | 86 | if (options.onSuccess) { 87 | return options.onSuccess(result); 88 | } 89 | 90 | return dispatch({ 91 | type: code === 200 ? ACTION.SUCCESS : ACTION.ERROR, 92 | payload: result, 93 | }); 94 | } catch (err) { 95 | console.error(err); 96 | 97 | if (__CLIENT__) { 98 | dispatch(updateLoadingAction(false)); 99 | } 100 | 101 | const { 102 | response: { data: res }, 103 | }: AxiosError = (err: AxiosError); 104 | 105 | const result: ApiDataType = (res: ApiDataType); 106 | 107 | // eslint-disable-next-line no-unused-vars 108 | const { code = 200, data, error } = result; 109 | 110 | if (options.onError) { 111 | return options.onError(result); 112 | } 113 | 114 | return dispatch({ type: ACTION.ERROR, payload: result }); 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /src/store/action.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { type GlobalStateType, type ApiDataType, type ThemeType } from 'types'; 3 | import { type Dispatch } from 'redux'; 4 | import { actionGenerator } from 'utils'; 5 | import { requestAction } from 'utils/request'; 6 | import cookies from 'utils/cookies'; 7 | 8 | export const UPDATE_TOKEN = '@@UPDATE_TOKEN'; 9 | export const updateTokenAction = (payload?: GlobalStateType) => ( 10 | dispatch: Dispatch, 11 | ) => { 12 | if (!payload?.accessToken || !payload?.refreshToken) { 13 | cookies.remove('accessToken', { path: '/' }); 14 | cookies.remove('refreshToken', { path: '/' }); 15 | } else { 16 | cookies.set('accessToken', payload?.accessToken, { path: '/' }); 17 | cookies.set('refreshToken', payload?.refreshToken, { path: '/' }); 18 | } 19 | 20 | return dispatch({ 21 | type: UPDATE_TOKEN, 22 | payload: { 23 | accessToken: payload?.accessToken, 24 | refreshToken: payload?.refreshToken, 25 | }, 26 | }); 27 | }; 28 | 29 | export const fetchTokenAction = () => (dispatch: Dispatch) => 30 | dispatch( 31 | updateTokenAction({ 32 | accessToken: cookies.get('accessToken'), 33 | refreshToken: cookies.get('refreshToken'), 34 | }), 35 | ); 36 | 37 | export const UPDATE_LOADING = '@@UPDATE_LOADING'; 38 | export const updateLoadingAction = (isLoading: boolean) => ( 39 | dispatch: Dispatch, 40 | ) => dispatch({ type: UPDATE_LOADING, payload: isLoading }); 41 | 42 | export const UPDATE_THEME = '@@UPDATE_THEME'; 43 | export const updateThemeAction = (theme: ThemeType = 'light') => ( 44 | dispatch: Dispatch, 45 | ) => { 46 | localStorage.setItem('theme', theme); 47 | 48 | if (document.documentElement) { 49 | document.documentElement.setAttribute('theme', theme); 50 | } 51 | 52 | return dispatch({ type: UPDATE_THEME, payload: theme }); 53 | }; 54 | 55 | export const RENEW_TOKEN = actionGenerator('@@RENEW_TOKEN'); 56 | export const renewTokenAction = (data: Object) => (dispatch: Dispatch) => 57 | dispatch( 58 | requestAction({ 59 | url: '/auth/renew-token', 60 | label: RENEW_TOKEN.NAME, 61 | method: 'POST', 62 | data, 63 | onSuccess: ({ data: res }: ApiDataType) => { 64 | // $FlowFixMe 65 | dispatch(updateTokenAction({ ...res })); 66 | }, 67 | onError: (_res: ApiDataType) => { 68 | dispatch(updateTokenAction()); 69 | }, 70 | }), 71 | ); 72 | 73 | export const GET_ME = actionGenerator('@@GET_ME'); 74 | export const getMeAction = () => async ( 75 | dispatch: Dispatch, 76 | getState: () => Object, 77 | ) => { 78 | const { 79 | global: { accessToken }, 80 | } = getState(); 81 | 82 | if (!accessToken) { 83 | return dispatch({ type: GET_ME.ERROR, payload: null }); 84 | } 85 | 86 | return dispatch( 87 | requestAction({ 88 | url: '/me', 89 | label: GET_ME.NAME, 90 | onSuccess: ({ data }: ApiDataType) => { 91 | dispatch({ 92 | type: GET_ME.SUCCESS, 93 | payload: data, 94 | }); 95 | }, 96 | onError: async ({ code, error }: ApiDataType) => { 97 | if (code === 401) { 98 | await dispatch(updateTokenAction()); 99 | } 100 | 101 | await dispatch({ type: GET_ME.ERROR, payload: error }); 102 | }, 103 | }), 104 | ); 105 | }; 106 | -------------------------------------------------------------------------------- /src/secure/jwt.private.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAyVDcJwZfwwAuVg9O02swoK0q5ryTvtCSfRwKmbdqOVfrHkqP 3 | D04BcinrcfJbcK2uwBeys00cpF+maVJcZG6KaUDOMaJRAfKD7pi6LayCRZlnxWeF 4 | E5INLfbRobajuivGfMq+5Gv7Hp++yvCQZWXOx3YxmZqYvKjM2BC3eLz1s+1UoCE8 5 | 0MAHCbP0+qkyskaZMgUGrp//nxB8t2rZYBIl7c7qZ6v2OElHmwOXKHbNdlxK+PBt 6 | KUy1AyGhm6vJJ+aLHPO9b/gNQPgEDEZ5NjxAG1d1a6KSzjFqjKY+kvEPKMLjOChR 7 | IGuoZ5dwv66hdRREHtEvAb08vLcabc/4uj7uc8LkVBDCUhHOhUzcOWFwKxpglnpJ 8 | oNecU/sPdq+dnlsfzZz2G8cfh7FTJzo97chtr83L5aMafJ8+CMvEN/LRO1erUdbV 9 | DrPqN0OoNh4h3tF15p9Kjzm7bnm6R7NNH7yV2XcLUTbzFzBdf58if+OEVGLsqLUn 10 | w/mHz19kEIdOi76tm2ZofoN2CjxMWzjr4PRMDixfY50faP8xlr35uUGPo484fH1n 11 | c3N1qA5rCIGn1JUSR8zh4HuMUfiDbOGglvz4LdBRHvJEUwNQipADTm4kxnwb5N3b 12 | agc2K2dySU2f918AAW6TTOzRPrUKkCBz6eVRZv5XNPWUxLP4jDmtiFOK/7kCAwEA 13 | AQKCAgBL2HYJEeK0QfYzIDNPfdvlPTijk7qKMmRuVEk+HpcboZ8IW4jIeFgdHeQB 14 | mxCORDFJV+RQnyXCW/MDTS9X+zmUbAEDPCcO98Jy+wFxwcOW2zP4cIb3l5f1kD7s 15 | kTXgJyvxyiTfRDtpD0A/jyCnwxKDnWkUeOVAdswuPyYQjAh50FmItt0FvMMqCa2r 16 | XmGmPujqqnAZDFivIfQK2mupQU0e6kzv4B60mA8tPM4PRaslA6KPZW2LGMY9tsb/ 17 | Mlpn+PjCYKYncCn2JzrHMSnroAl7A5lOgwKSQpXlBaEflUhj0ADxIGdoModQ+AJX 18 | hUB5Lrh3b9Io8pXWd5myc58+9AS710l0EI1ayQplejQI+TZ0IPoc0zQe1JaHSSOh 19 | 8DMe4vB+g44xhZmZ+GHnX0Q2U8qX8jYQRtvh0CahIXuio+8IC1VWDB+MagfTekQH 20 | uIeXMSpkrblXtPs5umCniOgnqoD+QOd+3rH76JvgJhBzNVMTAutP7yDDp2bLYoWM 21 | xOfMAZvv0XwfxhPQBLviYA6tTQs3EJJ/iyHhowYIn4Gso5sTuToptCVAx2YImJZi 22 | y584XS2x+clznn1PNEeEFhot6yoWIiZkxD64k7kWtRufC/4x6csL0KhPJhS6nRGy 23 | 7HHTIKksE3t+DrWOhr3wRxvC8qyYcDfv55UT3XeIAqLPPAo0sQKCAQEA5PZHl/uQ 24 | HvaKtDBTS0vYlqWfbCRaDKdLmYm7fUs4N5eIXHipIkeh99H8Bu1DA9KbjXtYv7ju 25 | E/NTv4f80I2kGgoJcIu7asf608BaBjpoWtDwaB8zwsuhu8yRnEboc5ey9j87YGHi 26 | WAPz+R4GVExK0T8G5OURUjtiBf6v7TrEB0Gi6Ds59yxjXZLt9VsxveQgCzrm200J 27 | 0FomjLQa64IwLrZbnM/21SAySr/HtJp7ZtSjVtfI2zUhbFCunco+jhdlvaUFOph7 28 | BZfYEyLUx/poeHD74ToIui5gHOYdw8MwmpVX6a56fcQmZseZH56WHv8Vya3yoEoH 29 | p5f7VbMd4wjbpQKCAQEA4RbQCSiuaTG4YQ/Qlzs2Ly+ZMxExbVQ8AeXVM54F++QT 30 | Zr29a9E24FWn+rg9HUkPNDcE1pB8iMc0fe9Rmyj52TM93wHKNnVM6UJ3ls6c+Sm3 31 | md0SwoAgn9YSo/4suTiecWSKelVq22Z3e6rxaugdvtv6r0r+oS07SdrVydEVLt+y 32 | DsLjSuu5y+2JN5GVLlWOhD7vbsS6zquFvgQ5nfVnTihp3asap0iSjNz0PhM/e/W3 33 | izo83ozbvY2y/69HLUyot5p0aVMQJpifwskvzP3g5ECmp4xNu26JB8tNQ77Gg20M 34 | sa2oaNrhKi+beQU/jWJoTClwdKfhnHWTyBGpmpPnhQKCAQBxDABRVA3Wm9fkG7Ak 35 | jzBDQUczd8dWVAuJpW2C8W6yVAkpzxGDMWcRGwaazO63pnbTJkGtd7tk8lE5UgVL 36 | W4PYr3f1r6g7kr2Pa3uHc7Muk9b/Mdi2pyAVv21tgb0nxZDA8Ht6nRnKZzlAmMh7 37 | Oqf+JGZdAZTJyzQczaFDOi6rfobWrtdx6OKuwurmp74piccghFaTlLfxvXEnK43X 38 | FhKAHd2h0TSICjuKmKIb0+J15Ss4p5YuVU9JUZTFp6O3OGotdprcUYj8O/qdiCcT 39 | DdojDXXvwF2qv1cJLb0oeOk4ieA8Kr+j9QMY4BgEeqKYb03spAwVhDe/UdTwfV2W 40 | STtZAoIBAG0Mx1dYnT8bto9XwSAzsKmlrtw93Stxnuzwc51hVtbVhczF6ip9HtAK 41 | z6o5bmpsxe/vI+nr8Fm34SeONYQtkvZ2y+fqDxTZOZzc9eFbUlOosWna4EbZGnU1 42 | mqxW7UYDN0gKhMiF75JCD2sbw4Ce2iJoowggFkLyDgJXAjKXmGKwP6zFIKdgkxBI 43 | ka0ZXCQpkTVi5E9PAo40LUSE+YEcrqMAE2IwjmYzU8TpG2+jlaMxxW2vlmPf7Gnm 44 | Z61WNu6scCUbsICCnJWGZghRvlXbPgR3igYshFBlWgArr9A9ciGkSH0gfDHosSPm 45 | ErXMJCEUZyFXBkOOPlC16hYI8JmSRJ0CggEBAKkqJo1MrX/i01v9wTb9yxTO6ksF 46 | 7V3YcfYeSJm6co45NaHHsndoHyDxc75aJY8zvm7hrvZ66Xojg6/QCQ8gZtI2QKBx 47 | msoy/6Bda0eQRd0bw4kFfswTiP2UUGIg5w6L86QOL0VkEth2DhUb5psBnJI9kqxM 48 | sEQW6yaH9/vmH1+RXBXxNeXWYI55BJo74WevqPHmZvFYI5yCMqRp2+cK3k+THioD 49 | D9mUxpCA371mQTiQFK3ckU7Y88xXStjCAcQ6j2Jfj03fhDII+SPxua31MhK2erWt 50 | TJrmdaI9w1jcxmj8+MRlbpBHyF0ho/VYGV0+d5NTFrS9JoXLx9fK5ITUUrs= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /src/tools/webpack/helper.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import webpack from 'webpack'; 3 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 4 | import LoadablePlugin from '@loadable/webpack-plugin'; 5 | import { isDev } from '../../config'; 6 | 7 | const cwd = process.cwd(); 8 | 9 | const rulesOfCss = [ 10 | { 11 | loader: MiniCssExtractPlugin.loader, 12 | options: { 13 | hmr: isDev, 14 | reloadAll: true, 15 | }, 16 | }, 17 | { 18 | loader: 'css', 19 | options: { 20 | importLoaders: 1, 21 | modules: { 22 | localIdentName: '[local]', 23 | context: resolve(cwd, 'src/client'), 24 | }, 25 | sourceMap: isDev, 26 | }, 27 | }, 28 | { 29 | loader: 'postcss', 30 | options: { 31 | sourceMap: isDev, 32 | config: { path: __dirname }, 33 | }, 34 | }, 35 | ]; 36 | 37 | export const getEntries = () => { 38 | let entries = [resolve(cwd, 'src/client/index.js')]; 39 | 40 | if (isDev) { 41 | entries = [ 42 | 'react-hot-loader/patch', 43 | 'webpack-hot-middleware/client?reload=true', 44 | ...entries, 45 | ]; 46 | } 47 | 48 | return entries; 49 | }; 50 | 51 | export const getOutPut = () => ({ 52 | path: resolve(cwd, 'public'), 53 | filename: isDev ? '[name].js' : '[name].[chunkhash:8].js', 54 | chunkFilename: isDev ? '[id].js' : '[id].[chunkhash:8].js', 55 | publicPath: '/', 56 | }); 57 | 58 | export const getPlugins = () => { 59 | const plugins = [ 60 | new webpack.NamedModulesPlugin(), 61 | new webpack.DefinePlugin({ 62 | __CLIENT__: true, 63 | __SERVER__: false, 64 | __DEV__: isDev, 65 | }), 66 | new MiniCssExtractPlugin({ 67 | filename: isDev ? '[name].css' : '[name].[contenthash:8].css', 68 | chunkFilename: isDev ? '[id].css' : '[id].[contenthash:8].css', 69 | ignoreOrder: false, 70 | }), 71 | new LoadablePlugin({ 72 | writeToDisk: true, 73 | filename: 'loadable-stats.json', 74 | }), 75 | ]; 76 | 77 | return plugins; 78 | }; 79 | 80 | export const getRules = () => { 81 | const rules = [ 82 | { 83 | test: /\.jsx?$/, 84 | exclude: /node_modules/, 85 | loader: 'babel', 86 | options: { cacheDirectory: isDev }, 87 | }, 88 | { 89 | test: /\.css$/, 90 | use: [...rulesOfCss], 91 | }, 92 | { 93 | test: /\.(scss|sass)$/, 94 | use: [ 95 | ...rulesOfCss, 96 | { 97 | loader: 'sass', 98 | options: { 99 | sourceMap: isDev, 100 | sassOptions: { 101 | includePaths: [resolve(cwd, 'src/client')], 102 | }, 103 | }, 104 | }, 105 | ], 106 | }, 107 | { 108 | test: /\.(woff2?|ttf|eot|svg)$/, 109 | loader: 'url', 110 | options: { limit: 10240, name: '[name].[hash:8].[ext]' }, 111 | }, 112 | { 113 | test: /\.(gif|png|jpe?g|webp)$/, 114 | loader: 'url', 115 | options: { limit: 10240, name: '[name].[hash:8].[ext]' }, 116 | }, 117 | ]; 118 | 119 | return rules; 120 | }; 121 | 122 | export const getResolver = () => ({ 123 | resolveLoader: { 124 | moduleExtensions: ['-loader'], 125 | }, 126 | resolve: { 127 | extensions: ['.json', '.js', '.jsx'], 128 | alias: { 129 | 'react-dom': '@hot-loader/react-dom', 130 | }, 131 | }, 132 | }); 133 | -------------------------------------------------------------------------------- /src/client/pages/Post/CreatePost/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { useEffect, useState } from 'react'; 3 | import { connect } from 'react-redux'; 4 | import { Redirect } from 'react-router-dom'; 5 | import ReactMde from 'react-mde'; 6 | import { toast } from 'react-toastify'; 7 | 8 | import Layout from 'components/Layout'; 9 | import TagsInput from 'components/TagsInput'; 10 | import { makeEmojiHtml } from 'components/MdViewer'; 11 | 12 | import * as action from './action'; 13 | 14 | import './styles.scss'; 15 | 16 | const CreatePost = ({ 17 | route: { title }, 18 | createPost: { post, error }, 19 | createPostAction, 20 | deleteLocalPostAction, 21 | }) => { 22 | useEffect(() => { 23 | if (error) { 24 | toast.error(error?.message); 25 | } 26 | 27 | return () => { 28 | deleteLocalPostAction(); 29 | }; 30 | }, [error]); 31 | 32 | const [titlePost, setTitlePost] = useState(''); 33 | 34 | const onTitlePostChange = ({ target: { value } }) => setTitlePost(value); 35 | 36 | const [description, setDescription] = useState(''); 37 | 38 | const onDescriptionChange = ({ target: { value } }) => setDescription(value); 39 | 40 | const [tags, setTags] = useState([]); 41 | 42 | const onTagsInputChange = (values) => { 43 | if (values.length > 5) { 44 | setTags([...tags]); 45 | 46 | return; 47 | } 48 | 49 | setTags([...values]); 50 | }; 51 | 52 | const [source, setSource] = useState(''); 53 | 54 | const [selectedTab, setSelectedTab] = useState('write'); 55 | 56 | const onInputChange = (value) => { 57 | setSource(value); 58 | }; 59 | 60 | const onPublish = () => { 61 | createPostAction({ 62 | title: titlePost, 63 | description, 64 | tags: tags.join(','), 65 | content: source, 66 | }); 67 | }; 68 | 69 | if (post) { 70 | return ; 71 | } 72 | 73 | return ( 74 | 75 | 81 | 82 | 88 | 89 | ( 96 |
97 | {tag} 98 | 99 | 110 |
111 | )} 112 | /> 113 | 114 | { 120 | const html = makeEmojiHtml(markdown); 121 | 122 | return html; 123 | }} 124 | /> 125 | 126 | 131 |
132 | ); 133 | }; 134 | 135 | const mapStateToProps = ({ global, postReducer: { createPost } }) => ({ 136 | global, 137 | createPost, 138 | }); 139 | 140 | const mapDispatchToProps = { 141 | createPostAction: action.createPostAction, 142 | deleteLocalPostAction: action.deleteLocalPostAction, 143 | }; 144 | 145 | export default connect(mapStateToProps, mapDispatchToProps)(CreatePost); 146 | -------------------------------------------------------------------------------- /src/tools/webpack/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import webpack from 'webpack'; 3 | import ImageminWebpackPlugin from 'imagemin-webpack-plugin'; 4 | import CompressionWebpackPlugin from 'compression-webpack-plugin'; 5 | import WebpackPwaManifest from 'webpack-pwa-manifest'; 6 | import TerserWebpackPlugin from 'terser-webpack-plugin'; 7 | import OfflinePlugin from 'offline-plugin'; 8 | import OptimizeCSSAssetsWebpackPlugin from 'optimize-css-assets-webpack-plugin'; 9 | import { 10 | getEntries, 11 | getOutPut, 12 | getPlugins, 13 | getRules, 14 | getResolver, 15 | } from './helper'; 16 | 17 | const cwd = process.cwd(); 18 | 19 | export default { 20 | mode: 'production', 21 | entry: getEntries(), 22 | output: getOutPut(), 23 | plugins: [ 24 | ...getPlugins(), 25 | new webpack.optimize.OccurrenceOrderPlugin(true), 26 | new webpack.optimize.ModuleConcatenationPlugin(), 27 | new webpack.HashedModuleIdsPlugin(), 28 | new ImageminWebpackPlugin({ 29 | test: /\.(jpe?g|png|gif|svg)$/i, 30 | minFileSize: 1024, 31 | pngquant: { quality: '70-100' }, 32 | }), 33 | new CompressionWebpackPlugin({ 34 | test: /\.(js|css|html|svg)?$/, 35 | threshold: 1024, 36 | cache: false, 37 | }), 38 | new WebpackPwaManifest({ 39 | name: 'Express React Boilerplate', 40 | short_name: 'ERB', 41 | description: 42 | '🔥 This is a tool that helps programmers create Express & React projects easily.', 43 | background_color: '#ffffff', 44 | theme_color: '#33cccc', 45 | inject: true, 46 | ios: true, 47 | icons: [ 48 | { 49 | src: resolve(cwd, 'public/assets/favicon-512x512.png'), 50 | sizes: [72, 96, 128, 144, 192, 384, 512], 51 | }, 52 | { 53 | src: resolve(cwd, 'public/assets/favicon-512x512.png'), 54 | sizes: [120, 152, 167, 180], 55 | ios: true, 56 | }, 57 | ], 58 | filename: 'site.webmanifest', 59 | start_url: '.', 60 | display: 'standalone', 61 | }), 62 | new OfflinePlugin({ 63 | autoUpdate: true, 64 | appShell: '/', 65 | relativePaths: false, 66 | updateStrategy: 'all', 67 | externals: ['/'], 68 | responseStrategy: 'network-first', 69 | }), 70 | ], 71 | module: { 72 | rules: getRules(), 73 | }, 74 | ...getResolver(), 75 | optimization: { 76 | namedModules: false, 77 | namedChunks: false, 78 | flagIncludedChunks: true, 79 | occurrenceOrder: true, 80 | usedExports: true, 81 | concatenateModules: true, 82 | noEmitOnErrors: true, 83 | minimize: true, 84 | removeAvailableModules: true, 85 | removeEmptyChunks: true, 86 | mergeDuplicateChunks: true, 87 | sideEffects: true, 88 | runtimeChunk: true, 89 | minimizer: [ 90 | new TerserWebpackPlugin({ 91 | parallel: true, 92 | sourceMap: false, 93 | extractComments: false, 94 | terserOptions: { 95 | compress: { 96 | booleans: true, 97 | pure_funcs: [ 98 | 'console.log', 99 | 'console.info', 100 | 'console.debug', 101 | 'console.warn', 102 | ], 103 | }, 104 | warnings: false, 105 | mangle: true, 106 | }, 107 | }), 108 | new OptimizeCSSAssetsWebpackPlugin({ 109 | assetNameRegExp: /\.optimize\.css$/g, 110 | cssProcessorPluginOptions: { 111 | preset: ['default', { discardComments: { removeAll: true } }], 112 | }, 113 | }), 114 | ], 115 | splitChunks: { 116 | chunks: 'all', 117 | cacheGroups: { 118 | vendor: { 119 | chunks: 'all', 120 | test: /[\\/]node_modules[\\/]/, 121 | enforce: true, 122 | name(module) { 123 | // get the name. E.g. node_modules/packageName/not/this/part.js 124 | // or node_modules/packageName 125 | const packageName = module.context.match( 126 | /[\\/]node_modules[\\/](.*?)([\\/]|$)/, 127 | )[1]; 128 | 129 | // npm package names are URL-safe, but some servers don't like @ symbols 130 | return `npm.${packageName.replace('@', '')}`; 131 | }, 132 | }, 133 | }, 134 | }, 135 | }, 136 | performance: { hints: false }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/client/pages/Post/PostDetail/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router-dom'; 4 | import { toast } from 'react-toastify'; 5 | import ReactMde from 'react-mde'; 6 | import moment from 'moment-timezone'; 7 | 8 | import Layout from 'components/Layout'; 9 | import MdViewer, { makeEmojiHtml } from 'components/MdViewer'; 10 | 11 | import { formatDate } from 'utils'; 12 | 13 | import * as action from './action'; 14 | import '../styles.scss'; 15 | 16 | const PostDetail = ({ 17 | match: { params }, 18 | global: { accessToken }, 19 | postDetail: { post, comments, error }, 20 | getPostDetailAction, 21 | getCommentsAction, 22 | postCommentAction, 23 | }) => { 24 | const { _id } = params; 25 | 26 | useEffect(() => { 27 | if (post?._id !== _id) { 28 | getPostDetailAction(_id); 29 | } 30 | 31 | if (comments?.length === 0 || post?._id !== _id) { 32 | getCommentsAction(_id); 33 | } 34 | 35 | if (error) { 36 | toast.error(error?.message); 37 | } 38 | }, []); 39 | 40 | const [source, setSource] = useState(''); 41 | 42 | const [selectedTab, setSelectedTab] = useState('write'); 43 | 44 | const onInputChange = (value) => { 45 | setSource(value); 46 | }; 47 | 48 | const onPostComment = () => { 49 | postCommentAction({ _id, comment: source }); 50 | 51 | setSource(''); 52 | 53 | setSelectedTab('write'); 54 | }; 55 | 56 | return ( 57 | 58 |
59 |

{post?.title}

60 | 61 |

62 | {`${formatDate(post?.publishAt)} - Published by `} 63 | 64 | {post?.user?.name} 65 | 66 |

67 | 68 |
69 | {post?.tags?.map((tag, i) => ( 70 | 71 | {tag} 72 | 73 | ))} 74 |
75 | 76 | 77 |
78 | 79 |
80 | 81 | {post && ( 82 |
83 |
Comments
84 | 85 | {!accessToken && ( 86 | <> 87 |
88 |
89 | Login to comment. 90 |
91 |
92 | 93 | )} 94 | 95 | {accessToken && ( 96 | <> 97 | { 103 | const html = makeEmojiHtml(markdown); 104 | 105 | return html; 106 | }} 107 | /> 108 | 109 | 114 | 115 | )} 116 | 117 | {comments?.map((comment) => ( 118 |
119 |
120 |
{comment.user?.name}
121 | 122 | 123 | 124 |
125 | {moment(comment.createAt || new Date()) 126 | .format('MMM DD, YYYY') 127 | .toString()} 128 |
129 |
130 |
131 | ))} 132 |
133 | )} 134 |
135 | ); 136 | }; 137 | 138 | const mapStateToProps = ({ global, postReducer: { postDetail } }) => ({ 139 | global, 140 | postDetail, 141 | }); 142 | 143 | const mapDispatchToProps = { 144 | getPostDetailAction: action.getPostDetailAction, 145 | getCommentsAction: action.getCommentsAction, 146 | postCommentAction: action.postCommentAction, 147 | }; 148 | 149 | export default connect(mapStateToProps, mapDispatchToProps)(PostDetail); 150 | -------------------------------------------------------------------------------- /public/assets/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 25 | 29 | 33 | 35 | 43 | 46 | 49 | 53 | 56 | 59 | 62 | 66 | 70 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import { resolve } from 'path'; 3 | import Express, { type Request, type Response } from 'express'; 4 | import bodyParser from 'body-parser'; 5 | import cors from 'cors'; 6 | import compression from 'compression'; 7 | import helmet from 'helmet'; 8 | import serveFavicon from 'serve-favicon'; 9 | import React from 'react'; 10 | import ReactDOMServer from 'react-dom/server'; 11 | import { StaticRouter } from 'react-router-dom'; 12 | import { renderRoutes, matchRoutes } from 'react-router-config'; 13 | import { LastLocationProvider } from 'react-router-last-location'; 14 | import { CookiesProvider } from 'react-cookie'; 15 | import { Provider } from 'react-redux'; 16 | import { Helmet } from 'react-helmet'; 17 | import { ChunkExtractor, ChunkExtractorManager } from '@loadable/server'; 18 | import renderHtml from './utils/render-html'; 19 | import routes from './routes'; 20 | import { 21 | passportMiddleware, 22 | notFoundErrorMiddleware, 23 | serverErrorMiddleware, 24 | } from './middlewares'; 25 | import api from './api'; 26 | import { isDev } from './config'; 27 | import configureStore from './store'; 28 | 29 | const app = Express(); 30 | 31 | app.use([ 32 | cors({ origin: true, credentials: false }), 33 | bodyParser.json(), 34 | bodyParser.urlencoded({ extended: true }), 35 | serveFavicon(resolve(process.cwd(), 'public/assets/favicon.ico')), 36 | compression(), 37 | helmet(), 38 | ]); 39 | 40 | app.use(Express.static(resolve(process.cwd(), 'public'))); 41 | 42 | if (isDev) { 43 | const { webpackMiddleware } = require('./middlewares'); 44 | 45 | app.use(webpackMiddleware()); 46 | } 47 | 48 | app.use( 49 | passportMiddleware([ 50 | /^(?!.*api).*/g, 51 | /^(?!.*^\/api\/auth\/logout)(\/api\/auth)/, 52 | /^(?!.*^\/api\/post\/create-post)(\/api\/post)/, 53 | /^(?!.*^\/api\/comment\/post-comment)(\/api\/comment)/, 54 | ]), 55 | ); 56 | 57 | app.use('/api', api); 58 | 59 | app.use('/api', serverErrorMiddleware()); 60 | 61 | app.use('/api', notFoundErrorMiddleware()); 62 | 63 | app.get('/*', async (req: Request, res: Response) => { 64 | const { store } = configureStore({ url: req.url }); 65 | 66 | const loadBranchData = (): Promise => { 67 | const branches = matchRoutes(routes, req.path); 68 | 69 | const promises = branches.map(({ route, match }) => { 70 | if (route.loadData) { 71 | return Promise.all( 72 | route 73 | .loadData({ params: match.params, getState: store.getState }) 74 | .map((action) => store.dispatch(action)), 75 | ); 76 | } 77 | 78 | return Promise.resolve(null); 79 | }); 80 | 81 | return Promise.all(promises); 82 | }; 83 | 84 | try { 85 | await loadBranchData(); 86 | 87 | const context = {}; 88 | 89 | const statsFile = resolve(process.cwd(), 'public/loadable-stats.json'); 90 | 91 | const extractor = new ChunkExtractor({ statsFile }); 92 | 93 | const App = ( 94 | 95 | 96 | 97 | 98 | 99 | {renderRoutes(routes)} 100 | 101 | 102 | 103 | 104 | 105 | ); 106 | 107 | const body = []; 108 | 109 | return ReactDOMServer.renderToStaticNodeStream(App) 110 | .on('data', (chunk) => { 111 | body.push(chunk.toString()); 112 | }) 113 | .on('error', (error) => { 114 | return res.status(404).send(error.message); 115 | }) 116 | .on('end', () => { 117 | const htmlContent = body.join(''); 118 | 119 | if (context.url) { 120 | res.status(301).setHeader('location', context.url); 121 | 122 | return res.end(); 123 | } 124 | 125 | const status = context.status === '404' ? 404 : 200; 126 | 127 | const initialState = store.getState(); 128 | 129 | const head = Helmet.renderStatic(); 130 | 131 | return res 132 | .status(status) 133 | .send(renderHtml({ head, extractor, htmlContent, initialState })); 134 | }); 135 | } catch (error) { 136 | console.error(error); 137 | 138 | console.error(`==> 😭 Rendering routes error: ${error}`); 139 | 140 | return res.status(404).send('Not Found :('); 141 | } 142 | }); 143 | 144 | export default app; 145 | -------------------------------------------------------------------------------- /src/client/pages/Introduce/profile.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | const site = (link: string, display?: string = 'Site') => 3 | `${display}`; 4 | 5 | const download = (link: string, display?: string = 'Download') => 6 | site(link, display); 7 | 8 | export const profile = ` 9 | # Introduce 10 | # **Huỳnh Trần Đăng Khoa** 11 | *[Steenify](https://steenify.com)* / *[Github](http://github.com/htdangkhoa)* / *[Linkedin](https://www.linkedin.com/in/khoa-đăng-7575a6136)* 12 | > Every problem has more than one way to solve it. 13 | 14 | ## Certification 15 | **May 27, 2016** 16 | Linux Operating System Certificate. 17 | 18 | **Apr 04, 2016** 19 | GDG Vietnam Certificate of completion Android course. 20 | 21 | ## Work history 22 | **Steenify | Oct, 2018 - Present** 23 | \`Android Developer\` / \`Backend Node.js Developer\` 24 | - Research new technology about Android. 25 | - Built an components, libraries for reduce the development time. 26 | - Guild co-worker how to develop android application. 27 | - Develop, build & release application. 28 | - Deploy server. 29 | 30 | ----- 31 | 32 | **BCA Studio | June, 2017 - Oct, 2018** 33 | \`Android Developer\` / \`Web Developer\` 34 | - Research new technology about Android. 35 | - Built an components, libraries for reduce the development time. 36 | - Guild co-worker how to develop android application. 37 | - Manage team, source and review code's co-worker. 38 | - Analysis system corporation. 39 | - Deploy server. 40 | - Develop, build & release application. 41 | 42 | ----- 43 | 44 | **Freelance | 2017 - Present** 45 | \`Freelance Mobile Developer\` 46 | - Analysis system corporation. 47 | - Develop, build & release application. 48 | 49 | ----- 50 | 51 | **C.A.N Group | Apr, 2016 - May, 2017** 52 | \`Hybrid Developer\` 53 | - Research about Ionic Framework. 54 | - Join to project, develop and maintenance. 55 | - Communicate with the customer to retrieve the request. 56 | - Develop, build & release application. 57 | `; 58 | 59 | export const projects = [ 60 | { 61 | pinned: true, 62 | source: ` 63 | **ERB - Express React boilerplate | Oct, 2019 - Dec, 2019 | ${site( 64 | 'https://htdangkhoa-erb.herokuapp.com', 65 | )} | ${download('https://github.com/htdangkhoa/erb', 'Github')}** 66 | \`Express\` / \`React\` / \`Node.js\` / \`ES6\` / \`Universal\` 67 | :fire: This is a tool that helps programmers create Express & React projects easily base on react-cool-starter. 68 | - Development 69 | - Review code 70 | - Optimize code 71 | - Bug Fixing 72 | - Deploy 73 | `, 74 | }, 75 | { 76 | pinned: false, 77 | source: ` 78 | **TMWorld | Aug, 2019 - Nov, 2019 | ${site( 79 | 'https://tmworld.app', 80 | )} | ${download( 81 | 'https://play.google.com/store/apps/details?id=com.tmworld.tmworld&hl=en', 82 | )}** 83 | \`Node.js\` / \`Javascript\` / \`ExpressJS\` / \`ES6\` / \`MongoDB\` / \`AWS\` 84 | Search for clubs easily. View club's upcoming meetings. Contact clubs in one click. Club visiting has never been so convenient. Never miss a meeting again. 85 | - Development 86 | - Review code 87 | - Optimize code 88 | - Bug Fixing 89 | - Deploy 90 | `, 91 | }, 92 | { 93 | pinned: false, 94 | source: ` 95 | **DreValet & DreFleet | Dec, 2018 - Sep, 2019 | ${download( 96 | 'https://play.google.com/store/apps/details?id=com.drevalet.dre&hl=en', 97 | 'Download DreValet', 98 | )} | ${download( 99 | 'https://play.google.com/store/apps/details?id=com.drevalet.drevalet', 100 | 'Download DreFleet', 101 | )}** 102 | \`Android\` / \`Kotlin\` / \`RxJava\` / \`RxAndroid\` / \`Firebase\` / \`Realm\` 103 | The app that gets you a trusted valet at the tap of a button! Enjoy your night out – we’ll get both you and your car home safely. 104 | - Development 105 | - Review code 106 | - Optimize code 107 | - Bug Fixing 108 | `, 109 | }, 110 | { 111 | pinned: false, 112 | source: ` 113 | **MarineTime - MPA | Jul, 2018 - May, 2019 | ${download( 114 | 'https://play.google.com/store/apps/details?id=sg.gov.mpa.marsg&hl=en', 115 | )}** 116 | \`Android\` / \`RxJava\` / \`RxAndroid\` 117 | This is the app that helps people to keep track of vessel time. 118 | - Analysis 119 | - Development 120 | - Review code 121 | - Optimize code 122 | - Bug Fixing 123 | `, 124 | }, 125 | { 126 | pinned: false, 127 | source: ` 128 | **EMA Components | Mar, 2018 - Apr, 2019** 129 | \`Android\` 130 | This project to provide component is customized. 131 | - Development 132 | - Review code 133 | - Optimize code 134 | - Bug Fixing 135 | `, 136 | }, 137 | { 138 | pinned: false, 139 | source: ` 140 | **8days | Jun, 2017 - Jul, 2018** 141 | \`Android\` / \`GraphQL\` / \`AES En-Decryption\` / \`AWS\` 142 | Application is a solution to help worker at factory can other food using QR Code technology. 143 | - Analysis 144 | - Development 145 | - Review code 146 | - Optimize code 147 | - Bug Fixing 148 | `, 149 | }, 150 | { 151 | pinned: false, 152 | source: ` 153 | **Recpic | Apr, 2016 - May, 2017** 154 | \`Ionic\` / \`Android\` / \`iOS\` / \`HTML\` / \`SCSS\` / \`Javascript\` 155 | This is the app that helps people to manage their spending. 156 | - Development 157 | - Optimize code 158 | - Bug Fixing 159 | `, 160 | }, 161 | ].sort((a, b) => { 162 | return Number(b.pinned) - Number(a.pinned); 163 | }); 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-react-boilerplate", 3 | "version": "4.2.1", 4 | "description": "Express react boilerplate", 5 | "keywords": [ 6 | "erb", 7 | "erb-gen", 8 | "express", 9 | "expressjs", 10 | "rest", 11 | "restful", 12 | "router", 13 | "app", 14 | "api", 15 | "react", 16 | "react-router", 17 | "redux", 18 | "template", 19 | "webpack", 20 | "universal", 21 | "boilerplate", 22 | "postcss", 23 | "css-modules", 24 | "pwa", 25 | "progressive web app", 26 | "offline" 27 | ], 28 | "homepage": "https://github.com/htdangkhoa/erb", 29 | "bugs": { 30 | "url": "https://github.com/htdangkhoa/erb/issues" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/htdangkhoa/erb.git" 35 | }, 36 | "license": "MIT", 37 | "author": "htdangkhoa (https://github.com/htdangkhoa)", 38 | "contributors": [ 39 | "huynhtran.dangkhoa@gmail.com" 40 | ], 41 | "main": "dist/index.js", 42 | "bin": { 43 | "erb-gen": "./bin/index.js" 44 | }, 45 | "scripts": { 46 | "preanalyze": "npm run stats", 47 | "analyze": "webpack-bundle-analyzer stats.json", 48 | "prebuild": "del-cli dist", 49 | "build": "BABEL_ENV=production babel --minified --no-comments src -D -d dist && npm run wp", 50 | "postbuild": "del-cli dist/**/__tests__", 51 | "clean": "del-cli \"public/*\" \"!public/assets\" \"!public/locales\" \"!public/robots.txt\" \"!public/googledb37d62693032295.html\"", 52 | "predev": "del-cli dist && npm run clean", 53 | "dev": "env-cmd -e development nodemon --trace-warnings --inspect=0.0.0.0:58585 src/index.js --exec babel-node", 54 | "eslint": "eslint src --ext .js", 55 | "start": "env-cmd -e production node dist/index.js", 56 | "stats": "env-cmd -e production babel-node ./node_modules/.bin/webpack --config src/tools/webpack.config.js --progress --colors --bail --json > stats.json", 57 | "storybook": "start-storybook --ci", 58 | "test": "jest -u", 59 | "prewp": "npm run clean", 60 | "wp": "env-cmd -e production babel-node ./node_modules/.bin/webpack --config src/tools/webpack/webpack.config.js --progress --colors --bail" 61 | }, 62 | "nodemonConfig": { 63 | "ignore": [ 64 | "dist", 65 | "public", 66 | "src/client", 67 | "src/store" 68 | ] 69 | }, 70 | "browserslist": { 71 | "production": [ 72 | "ie 11", 73 | ">0.1%", 74 | "not dead", 75 | "not op_mini all" 76 | ], 77 | "development": [ 78 | "ie 11", 79 | "last 1 chrome version", 80 | "last 1 firefox version", 81 | "last 1 safari version" 82 | ] 83 | }, 84 | "jest": { 85 | "globals": { 86 | "__DEV__": false 87 | }, 88 | "moduleNameMapper": { 89 | "\\.(css|less|scss|sss|styl)$": "/src/tools/jest/styles-mock.js", 90 | ".*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/src/tools/jest/assets-mock.js" 91 | } 92 | }, 93 | "dependencies": { 94 | "@fortawesome/fontawesome-free": "^5.13.1", 95 | "@hot-loader/react-dom": "^16.10.2", 96 | "@loadable/component": "^5.13.1", 97 | "@loadable/server": "^5.13.1", 98 | "asset-require-hook": "^1.2.0", 99 | "autoprefixer": "^9.8.4", 100 | "axios": "^0.19.0", 101 | "bcrypt": "^5.0.0", 102 | "body-parser": "^1.19.0", 103 | "bootstrap": "^4.3.1", 104 | "commander": "^5.0.0", 105 | "compression": "^1.7.4", 106 | "connected-react-router": "^6.5.2", 107 | "cors": "^2.8.5", 108 | "css-modules-require-hook": "^4.2.3", 109 | "del": "^5.1.0", 110 | "env-cmd": "^10.1.0", 111 | "express": "^4.17.1", 112 | "git-clone": "^0.1.0", 113 | "helmet": "^3.23.3", 114 | "history": "^4.10.1", 115 | "html-minifier": "^4.0.0", 116 | "i18next": "^19.5.4", 117 | "i18next-browser-languagedetector": "^5.0.0", 118 | "i18next-xhr-backend": "^3.2.0", 119 | "jsonwebtoken": "^8.5.1", 120 | "lodash": "^4.17.19", 121 | "moment-timezone": "^0.5.27", 122 | "mongodb": "^3.5.8", 123 | "node-sass": "^4.13.0", 124 | "offline-plugin": "^5.0.7", 125 | "passport": "^0.4.0", 126 | "passport-jwt": "^4.0.0", 127 | "prop-types": "^15.7.2", 128 | "react": "^16.10.2", 129 | "react-cookie": "^4.0.1", 130 | "react-dom": "^16.11.0", 131 | "react-emoji-render": "^1.2.0", 132 | "react-helmet": "^6.0.0", 133 | "react-hot-loader": "^4.12.15", 134 | "react-html-parser": "^2.0.2", 135 | "react-i18next": "^11.7.0", 136 | "react-mde": "^10.0.3", 137 | "react-paginate": "^6.3.2", 138 | "react-redux": "^7.1.1", 139 | "react-router-config": "^5.1.1", 140 | "react-router-dom": "^5.1.2", 141 | "react-router-last-location": "^2.0.1", 142 | "react-spinners": "^0.9.0", 143 | "react-switch": "^5.0.1", 144 | "react-toastify": "^6.0.8", 145 | "reactstrap": "^8.5.1", 146 | "redux": "^4.0.4", 147 | "redux-devtools-extension": "^2.13.8", 148 | "redux-form": "^8.2.6", 149 | "redux-thunk": "^2.3.0", 150 | "serialize-javascript": "^4.0.0", 151 | "serve-favicon": "^2.5.0", 152 | "showdown": "^1.9.1", 153 | "universal-cookie": "^4.0.2", 154 | "validator": "^13.0.0" 155 | }, 156 | "devDependencies": { 157 | "@babel/cli": "^7.10.4", 158 | "@babel/core": "^7.10.4", 159 | "@babel/node": "^7.10.4", 160 | "@babel/plugin-proposal-optional-chaining": "^7.10.4", 161 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 162 | "@babel/plugin-transform-runtime": "^7.10.4", 163 | "@babel/plugin-transform-spread": "^7.10.4", 164 | "@babel/polyfill": "^7.10.4", 165 | "@babel/preset-env": "^7.10.4", 166 | "@babel/preset-flow": "^7.10.4", 167 | "@babel/preset-react": "^7.10.4", 168 | "@babel/runtime": "^7.10.4", 169 | "@loadable/babel-plugin": "^5.13.0", 170 | "@loadable/webpack-plugin": "^5.13.0", 171 | "@storybook/react": "^5.2.8", 172 | "address": "^1.1.2", 173 | "babel-eslint": "^10.0.3", 174 | "babel-loader": "^8.0.6", 175 | "babel-plugin-module-resolver": "^4.0.0", 176 | "compression-webpack-plugin": "^4.0.0", 177 | "css-loader": "^3.6.0", 178 | "del-cli": "^3.0.1", 179 | "eslint": "^7.4.0", 180 | "eslint-config-airbnb-base": "^14.0.0", 181 | "eslint-config-prettier": "^6.5.0", 182 | "eslint-import-resolver-babel-module": "^5.1.0", 183 | "eslint-plugin-flowtype": "^5.2.0", 184 | "eslint-plugin-import": "^2.22.0", 185 | "eslint-plugin-jest": "^23.18.0", 186 | "eslint-plugin-prettier": "^3.1.4", 187 | "eslint-plugin-react": "^7.20.3", 188 | "flow-bin": "^0.129.0", 189 | "friendly-errors-webpack-plugin": "^1.7.0", 190 | "imagemin-webpack-plugin": "^2.4.2", 191 | "jest": "^26.1.0", 192 | "jest-css-modules": "^2.1.0", 193 | "lodash-webpack-plugin": "^0.11.5", 194 | "mini-css-extract-plugin": "^0.9.0", 195 | "nodemon": "^2.0.1", 196 | "optimize-css-assets-webpack-plugin": "^5.0.3", 197 | "postcss": "^7.0.25", 198 | "postcss-loader": "^3.0.0", 199 | "prettier": "^2.0.2", 200 | "react-dev-utils": "^10.2.1", 201 | "react-test-renderer": "^16.12.0", 202 | "sass-loader": "^9.0.2", 203 | "tcp-port-used": "^1.0.1", 204 | "terser-webpack-plugin": "^3.0.6", 205 | "url-loader": "^4.0.0", 206 | "webpack": "^4.29.0", 207 | "webpack-bundle-analyzer": "^3.6.0", 208 | "webpack-cli": "^3.3.12", 209 | "webpack-dev-middleware": "^3.7.2", 210 | "webpack-hot-middleware": "^2.25.0", 211 | "webpack-pwa-manifest": "^4.2.0", 212 | "webpackbar": "^4.0.0" 213 | }, 214 | "engines": { 215 | "node": ">=10.13.0" 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/client/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Helmet } from 'react-helmet'; 4 | import { Redirect, withRouter, NavLink, Link } from 'react-router-dom'; 5 | import { 6 | useLastLocation, 7 | RedirectWithoutLastLocation, 8 | } from 'react-router-last-location'; 9 | import Switch from 'react-switch'; 10 | import { Collapse } from 'reactstrap'; 11 | 12 | import PropTypes from 'prop-types'; 13 | import * as globalAction from 'store/action'; 14 | import './styles.scss'; 15 | 16 | const Child = ({ 17 | title, 18 | children, 19 | className = '', 20 | showSidebar = true, 21 | location: { pathname }, 22 | global: { theme, accessToken, user }, 23 | updateThemeAction, 24 | updateTokenAction, 25 | }) => { 26 | const onChangeTheme = (checked) => { 27 | updateThemeAction(checked ? 'dark' : 'light'); 28 | }; 29 | 30 | const [collapsed, setCollapsed] = useState(true); 31 | 32 | const toggleNavbar = () => setCollapsed(!collapsed); 33 | 34 | return ( 35 | <> 36 | 37 |
38 |
39 | {showSidebar && ( 40 |
41 |
42 | 45 |
46 | 47 | 48 |
49 | 50 |

KBlog

51 | 52 | 53 | 58 | 59 |
60 | } 61 | uncheckedIcon={ 62 |
63 | 64 |
65 | } 66 | onColor='#fbfbff' 67 | offColor='#222725' 68 | onHandleColor='#449dd1' 69 | offHandleColor='#449dd1' 70 | handleDiameter={20} 71 | /> 72 |
73 | 74 |

75 | Sharing experiences, knowledge, and case studies help people 76 | create more professional applications and products. 77 |

78 | 79 |
80 |
Account
81 | 82 | {!accessToken && ( 83 | <> 84 |
    85 |
  • 86 | 87 | Login 88 | 89 |
  • 90 | 91 |
  • 92 | 95 | Register 96 | 97 |
  • 98 |
99 | 100 | )} 101 | 102 | {user && ( 103 | <> 104 |
    105 |
  • 106 | 109 | {user?.name} 110 | 111 |
  • 112 | 113 |
  • 114 | 117 | Create post 118 | 119 |
  • 120 | 121 |
  • 122 | { 126 | updateTokenAction(); 127 | }}> 128 | Logout 129 | 130 |
  • 131 |
132 | 133 | )} 134 |
135 | 136 |
137 |
Menu
138 | 139 |
    140 |
  • 141 | { 145 | return path.match(/^\/$/) || path.match(/^\/p\//); 146 | }}> 147 | Posts 148 | 149 |
  • 150 | {/*
  • 151 | 152 | Questions 153 | 154 |
  • */} 155 |
  • 156 | 159 | Introduce 160 | 161 |
  • 162 |
  • 163 | 164 | Contact 165 | 166 |
  • 167 |
168 |
169 | 170 |
171 |
Links
172 | 173 | 194 |
195 | 196 |
197 | )} 198 | 199 |
200 |
{children}
201 |
202 |
203 | 204 | 205 | ); 206 | }; 207 | 208 | const Layout = (props) => { 209 | const { 210 | needLogin, 211 | returnPath = '/', 212 | location: { pathname }, 213 | global: { accessToken, refreshToken, user }, 214 | fetchTokenAction, 215 | renewTokenAction, 216 | getMeAction, 217 | } = props; 218 | 219 | const lastLocation = useLastLocation(); 220 | 221 | useEffect(() => { 222 | fetchTokenAction(); 223 | 224 | if (refreshToken) { 225 | renewTokenAction({ refreshToken }); 226 | } 227 | 228 | if (!user) { 229 | getMeAction(); 230 | } 231 | 232 | // Reset scroll. 233 | window.scrollTo(0, 0); 234 | }, []); 235 | 236 | if (needLogin && !accessToken) { 237 | return ; 238 | } 239 | 240 | if (pathname === '/login' && accessToken) { 241 | return ( 242 | 243 | ); 244 | } 245 | 246 | return ; 247 | }; 248 | 249 | Layout.propTypes = { 250 | title: PropTypes.string, 251 | needLogin: PropTypes.bool, 252 | returnPath: PropTypes.string, 253 | children: PropTypes.node, 254 | className: PropTypes.string, 255 | showSidebar: PropTypes.bool, 256 | }; 257 | 258 | const mapStateToProps = ({ global }) => ({ global }); 259 | 260 | const mapDispatchToProps = { 261 | fetchTokenAction: globalAction.fetchTokenAction, 262 | renewTokenAction: globalAction.renewTokenAction, 263 | getMeAction: globalAction.getMeAction, 264 | updateThemeAction: globalAction.updateThemeAction, 265 | updateTokenAction: globalAction.updateTokenAction, 266 | }; 267 | 268 | export default connect(mapStateToProps, mapDispatchToProps)(withRouter(Layout)); 269 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

erb

2 | 3 |

🔥 🔥 🔥 Express react boilerplate 🔥 🔥 🔥

4 | 5 |

6 | 7 | dependency status 8 | 9 | 10 | 11 | devDependency status 12 | 13 | 14 | 15 | ESLint: airbnb-base 16 | 17 | 18 | 19 | code style: prettier 20 | 21 | 22 | 23 | github actions status 24 | 25 | 26 | 27 | CodeFactor 28 | 29 | 30 | 31 | MIT licensed 32 | 33 |

34 | 35 |

36 | 37 | NPM 38 | 39 |

40 | 41 | ## Features 42 | 43 | - Server side [(Express)](https://expressjs.com). 44 | - NoSQL database [(MongoDB)](mongodb.com). 45 | - Client side [(React)](https://reactjs.org). 46 | - Universal routing [(react-router)](https://github.com/ReactTraining/react-router). 47 | - State management [(Redux)](https://redux.js.org). 48 | - Redux debugging tools [(redux-devtools)](https://github.com/reduxjs/redux-devtools). 49 | - Tweak React components in real time [(react-hot-loader)](https://github.com/gaearon/react-hot-loader). 50 | - SEO [(react-helmet)](https://github.com/nfl/react-helmet). 51 | - The recommended Code Splitting library for React [(loadable-components)](https://github.com/gregberge/loadable-components). 52 | - Progressive web app [(offline-plugin)](https://github.com/NekR/offline-plugin). 53 | - Promise based HTTP client for the browser and NodeJS [(axios)](https://github.com/axios/axios). 54 | - Internationalization [(i18next)](https://www.i18next.com/) & [(react-i18next)](https://github.com/i18next/react-i18next). 55 | - A tool for transforming CSS with JavaScript [(PostCSS)](https://postcss.org/). 56 | - PostCSS plugin to parse CSS and add vendor prefixes to CSS rules using values from Can I Use. It is recommended by Google and used in Twitter and Alibaba [(autoprefixer)](https://github.com/postcss/autoprefixer). 57 | - Compiles CSS Modules in runtime [(css-modules-require-hook)](https://github.com/css-modules/css-modules-require-hook). 58 | - Allows files required by node that match a given set of extensions to be returned as either a data URI, or a custom filename [(asset-require-hook)](https://github.com/aribouius/asset-require-hook). 59 | - Developing UI components [(Storybook)](https://storybook.js.org/). 60 | - Light & Dark theme. 61 | - Data fetching from server-side. 62 | - Compressing images with imagemin [(imagemin-webpack-plugin)](https://github.com/Klathmon/imagemin-webpack-plugin). 63 | - Unit testing [(Jest)](https://github.com/facebook/jest). 64 | - ES6. 65 | - The optional chaining operator provides a way to simplify accessing values through connected objects when it's possible that a reference or function may be undefined or null [(@babel/plugin-proposal-optional-chaining)](https://babeljs.io/docs/en/babel-plugin-proposal-optional-chaining). 66 | - Type checker for javascript [(Flow)](https://flow.org/). 67 | - Find and fix problems in your javascript code [(ESlint)](https://eslint.org/). 68 | - Code formatter [(Prettier)](https://prettier.io/). 69 | - Automate your workflow from idea to production [(Github Actions)](https://github.com/features/actions). 70 | - VSCode debugging. 71 | 72 | ## Requirements 73 | 74 | - [Node](https://nodejs.org/en/) >= 10.13.0 75 | 76 | ## Structure 77 | 78 | ``` 79 | . 80 | ├── public # Express server static path 81 | │ ├── assets # All favicon resources 82 | │ ├── locales # All of i18n resources 83 | │ └── robots.txt # A robots.txt file tells search engine crawlers which pages or files the crawler can or can't request from your site. 84 | ├── src # App source code 85 | │ ├── api # All of restful API 86 | │ ├── client # Client scope 87 | │ │ ├── app # App root component 88 | │ │ ├── assets # Assets (e.g. images, fonts etc.) 89 | │ │ ├── components # Reusable components 90 | │ │ ├── pages # Page components 91 | │ │ ├── stories # UI components with Storybook 92 | │ │ ├── themes # App-wide style 93 | │ │ ├── vendor # 3rd libraries for client 94 | │ │ └── index.js # App bootstrap and rendering (webpack entry) 95 | │ ├── middlewares # All of express middleware 96 | │ ├── model # Data transfer object 97 | │ ├── mongo # MongoDB configuration 98 | │ ├── secure # All of security (e.g passport configuration, jsonwebtoken etc.) 99 | │ ├── store # Store configuration for both client and server side 100 | │ ├── tools # Project related configurations 101 | │ │ ├── jest # Jest configurations 102 | │ │ ├── webpack # Webpack configurations 103 | │ │ ├── hooks.js # Assets require hooks 104 | │ │ └── postcss.config.js # PostCSS configuration 105 | │ ├── types # All of type for flow 106 | │ ├── utils # App-wide utils 107 | │ ├── config.js # Configuration entry point loaded from .env file 108 | │ ├── i18n.js # I18next configuration 109 | │ ├── index.js # App entry point 110 | │ ├── routes.js # Routes configuration for both client and server side 111 | │ └── server.js # Express server 112 | │── .babelrc # Babel configuration. 113 | │── .env-cmdrc.json # All of environments configuration. 114 | │── .eslintrc.json # Eslint configuration. 115 | │── .flowconfig # Flow type configuration. 116 | └── .prettierrc.json # Prettier configuration. 117 | ``` 118 | 119 | ## Installation 120 | 121 | ```bash 122 | $ yarn global add express-react-boilerplate 123 | # or (sudo) npm install -g express-react-boilerplate 124 | ``` 125 | 126 | ## Getting Started 127 | 128 | **1. Usage:** 129 | 130 | ```bash 131 | $ erb-gen --help 132 | 133 | Usage: erb-gen [options] 134 | 135 | Options: 136 | -v, --version output the version number 137 | -d, --dir project's directory. (default: ".") 138 | -n, --name project's name. (default: "express-react-boilerplate") 139 | -h, --help output usage information 140 | 141 | Examples: 142 | $ erb-gen 143 | $ erb-gen --name example 144 | ``` 145 | 146 | **2. Install dependencies:** 147 | 148 | ```bash 149 | $ cd 150 | 151 | $ yarn 152 | # or npm install 153 | ``` 154 | 155 | **3. Run it:** 156 | 157 | ```bash 158 | $ yarn dev 159 | # or npm run dev 160 | ``` 161 | 162 | ## Build 163 | 164 | ```bash 165 | $ yarn build 166 | # or npm run build 167 | ``` 168 | 169 | --- 170 | 171 | > **NOTE**: You can change environment variables in `.env-cmdrc.json` file. 172 | 173 | ## Scripts 174 | 175 | | Script | Description | 176 | | ------------- | ------------------------------------------------------------------------------------- | 177 | | dev | Start the development server. | 178 | | dev `--serve` | Start the development server and open browser. | 179 | | start | Start the production server. | 180 | | build | Remove the previous bundled files and bundle it (include client & server) to `dist/`. | 181 | | wp | Bundle client to `dist/`. | 182 | | analyze | Visualize the contents of all your bundles. | 183 | | storybook | Start the storybook server. | 184 | | test | Run testing. | 185 | | eslint | Find problems in your JavaScript code. | 186 | 187 | ## Enable/Disable offline 188 | 189 | - In `src/tools/webpack/webpack.config.prod.js`: 190 | 191 | ```js 192 | if (isDev) { 193 | ... 194 | } else { 195 | plugins = [ 196 | ..., 197 | // Comment this plugin if you want to disable offline. 198 | new OfflinePlugin({ 199 | autoUpdate: true, 200 | appShell: '/', 201 | relativePaths: false, 202 | updateStrategy: 'all', 203 | externals: ['/'], 204 | }) 205 | ] 206 | } 207 | ``` 208 | 209 | - At the end of `src/client/index.js`: 210 | 211 | ```js 212 | if (!__DEV__) { 213 | require('offline-plugin/runtime').install(); // Comment this line if you want to disable offline. 214 | } 215 | ``` 216 | 217 | ## Supported Browsers 218 | 219 | By default, the generated project supports all modern browsers. Support for Internet Explorer 9, 10, and 11 requires polyfills. For a set of polyfills to support older browsers, use [react-app-polyfill](https://github.com/facebook/create-react-app/tree/master/packages/react-app-polyfill). 220 | 221 | ```bash 222 | $ yarn add react-app-polyfill 223 | # or npm install --save react-app-polyfill 224 | ``` 225 | 226 | You can import the entry point for the minimal version you intend to support to ensure that the minimum language features are present that are required to use in your project. For example, if you import the IE9 entry point, this will include IE10 and IE11 support. 227 | 228 | ### **Internet Explorer 9** 229 | 230 | ```js 231 | // This must be the first line in /src/client/app/index.js 232 | import 'react-app-polyfill/ie9'; 233 | import 'react-app-polyfill/stable'; 234 | 235 | // ... 236 | ``` 237 | 238 | ### **Internet Explorer 11** 239 | 240 | ```js 241 | // This must be the first line in /src/client/app/index.js 242 | import 'react-app-polyfill/ie11'; 243 | import 'react-app-polyfill/stable'; 244 | 245 | // ... 246 | ``` 247 | 248 | ## CSS variables 249 | 250 | By default, the generated project supports all modern browsers. Support for Internet Explorer 9, 10, and 11 requires polyfills. For a set of polyfills to support older browsers, use [css-vars-ponyfill](https://github.com/jhildenbiddle/css-vars-ponyfill). 251 | 252 | ```js 253 | // In /src/client/vendor/index.js 254 | import cssVars 'css-vars-ponyfill'; 255 | // ... your css/scss files. 256 | cssVars({ 257 | silent: !__DEV__, 258 | ..., // https://jhildenbiddle.github.io/css-vars-ponyfill/#/?id=options 259 | }); 260 | ``` 261 | 262 | ## Type Checking For Editor 263 | 264 | - [Visual Studio Code](https://flow.org/en/docs/editors/vscode/) 265 | - [Atom](https://flow.org/en/docs/editors/atom/) 266 | - [Sublime Text](https://flow.org/en/docs/editors/sublime-text/) 267 | - [Etc.](https://flow.org/en/docs/editors/) 268 | 269 | ## Contributors 270 | 271 | - [htdangkhoa](https://github.com/htdangkhoa) 272 | 273 | ## Special Thanks 274 | 275 | - [(react-cool-starter) - wellyshen](https://github.com/wellyshen/react-cool-starter) 276 | 277 | ## License 278 | 279 | MIT License 280 | 281 | Copyright (c) 2019 Huỳnh Trần Đăng Khoa 282 | 283 | Permission is hereby granted, free of charge, to any person obtaining a copy 284 | of this software and associated documentation files (the "Software"), to deal 285 | in the Software without restriction, including without limitation the rights 286 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 287 | copies of the Software, and to permit persons to whom the Software is 288 | furnished to do so, subject to the following conditions: 289 | 290 | The above copyright notice and this permission notice shall be included in all 291 | copies or substantial portions of the Software. 292 | 293 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 294 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 295 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 296 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 297 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 298 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 299 | SOFTWARE. 300 | --------------------------------------------------------------------------------