├── .gitattributes ├── .npmrc ├── .dockerignore ├── .eslintignore ├── .stylelintignore ├── src ├── app │ ├── modules │ │ ├── medium │ │ │ ├── Post.scss │ │ │ ├── types.js │ │ │ ├── actions │ │ │ │ └── MediumActions.js │ │ │ ├── reducers │ │ │ │ └── MediumReducer.js │ │ │ ├── Post.js │ │ │ └── Index.js │ │ ├── header │ │ │ ├── Header.scss │ │ │ └── Header.js │ │ ├── home │ │ │ ├── types.js │ │ │ ├── actions │ │ │ │ └── HomeActions.js │ │ │ ├── reducers │ │ │ │ └── HomeReducer.js │ │ │ ├── Post.js │ │ │ └── Home.js │ │ ├── toidicodedao │ │ │ ├── types.js │ │ │ ├── actions │ │ │ │ └── CodeDaoActions.js │ │ │ ├── reducers │ │ │ │ └── CodeDaoReducer.js │ │ │ ├── Post.js │ │ │ └── Index.js │ │ ├── loading │ │ │ └── Loading.tsx │ │ ├── radio │ │ │ ├── Radio.scss │ │ │ └── Radio.js │ │ ├── not-found │ │ │ └── NotFound.tsx │ │ ├── landing-page │ │ │ ├── Header.scss │ │ │ ├── Header.jsx │ │ │ ├── LandingPage.js │ │ │ └── LandingPage.scss │ │ ├── footer │ │ │ └── Footer.tsx │ │ ├── error │ │ │ └── Error.tsx │ │ ├── new-post │ │ │ └── NewPost.js │ │ ├── login │ │ │ └── Login.js │ │ ├── my-confess │ │ │ └── MyConfess.js │ │ ├── change │ │ │ └── Change.js │ │ ├── search │ │ │ └── Search.js │ │ └── send │ │ │ └── Send.js │ ├── reducers │ │ ├── AuthReducer.js │ │ └── index.js │ ├── Main.spec.js │ ├── Main.tsx │ ├── utils │ │ ├── shared │ │ │ ├── disqus │ │ │ │ └── DisqusComponent.js │ │ │ └── auth │ │ │ │ └── withAuthRouteComponent.js │ │ └── browser │ │ │ └── LocalStorage.ts │ ├── App.scss │ ├── App.tsx │ └── Routes.js ├── store.js ├── firebase.js ├── index.js └── index.html ├── public ├── loading-logo.png ├── assets │ ├── favicon.ico │ ├── images │ │ ├── index.png │ │ ├── thumb.jpg │ │ ├── fpt-logo.png │ │ ├── logo-radio.png │ │ ├── golang-react.jpg │ │ ├── toidicodedao.png │ │ ├── radio-background.jpg │ │ └── fptuhcm-confessions.png │ ├── firebase-messaging-sw.js │ └── manifest.json ├── _redirects ├── loader.css └── sw.js ├── .env ├── .env.example ├── .env.production ├── .lintstagedrc ├── .editorconfig ├── scripts └── deploy.sh ├── jsconfig.json ├── .babelrc ├── .gitignore ├── docker-compose.yml ├── webpack ├── production │ ├── webpack.server.js │ ├── webpack.client.js │ └── webpack.csr.js ├── development │ └── webpack.development.js └── common │ ├── webpack.core.js │ └── webpack.common.js ├── .prettierrc ├── dev.Dockerfile ├── Jenkinsfile ├── .github └── workflows │ └── nodejs.yml ├── tsconfig.json ├── Dockerfile ├── .vscode └── settings.json ├── LICENSE ├── test.js ├── README.md ├── .stylelintrc ├── .eslintrc ├── server ├── index.js ├── engine │ └── index.js └── render │ └── index.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | dist 4 | .vscode 5 | yarn.lock 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # dist 2 | dist 3 | /dist 4 | 5 | # build 6 | build 7 | /build 8 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dis 3 | build 4 | *.* 5 | !*.scss 6 | !*.sass 7 | -------------------------------------------------------------------------------- /src/app/modules/medium/Post.scss: -------------------------------------------------------------------------------- 1 | a[href*="logrocket"] { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /public/loading-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/loading-logo.png -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=3001 3 | API_BASE_URL=https://api.fuhcm.com 4 | FB_TAGNAME=fptuc 5 | -------------------------------------------------------------------------------- /public/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/favicon.ico -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | PORT=3000 3 | API_BASE_URL=https://api.fuhcm.com 4 | FB_TAGNAME=fptuc 5 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | PORT=3000 3 | API_BASE_URL=https://api.fuhcm.com 4 | FB_TAGNAME=fptuc 5 | -------------------------------------------------------------------------------- /public/assets/images/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/index.png -------------------------------------------------------------------------------- /public/assets/images/thumb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/thumb.jpg -------------------------------------------------------------------------------- /public/assets/images/fpt-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/fpt-logo.png -------------------------------------------------------------------------------- /public/assets/images/logo-radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/logo-radio.png -------------------------------------------------------------------------------- /public/assets/images/golang-react.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/golang-react.jpg -------------------------------------------------------------------------------- /public/assets/images/toidicodedao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/toidicodedao.png -------------------------------------------------------------------------------- /public/assets/images/radio-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/radio-background.jpg -------------------------------------------------------------------------------- /public/assets/images/fptuhcm-confessions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fuhcm/fptu-app/HEAD/public/assets/images/fptuhcm-confessions.png -------------------------------------------------------------------------------- /src/app/modules/header/Header.scss: -------------------------------------------------------------------------------- 1 | .ant-drawer-body { 2 | padding-left: 0; 3 | padding-top: 16px; 4 | } 5 | 6 | .ant-drawer-content { 7 | background: #001528; 8 | } 9 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "src/**/*.{js,jsx,ts}": ["prettier --write", "eslint --fix", "git add"], 3 | "src /**/*.{scss,sass}": ["prettier --write", "stylelint -fix", "git add"] 4 | } 5 | -------------------------------------------------------------------------------- / .editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ssh root@35.247.181.39 < { 11 | return state; 12 | }; 13 | -------------------------------------------------------------------------------- /src/app/modules/medium/types.js: -------------------------------------------------------------------------------- 1 | export const GET_MEDIUM_ARTICLE_LOADING = "GET_MEDIUM_ARTICLE_LOADING"; 2 | export const GET_MEDIUM_ARTICLE_SUCCESS = "GET_MEDIUM_ARTICLE_SUCCESS"; 3 | export const GET_MEDIUM_ARTICLE_FAILURE = "GET_MEDIUM_ARTICLE_FAILURE"; 4 | export const LOAD_MORE_ARTICLE = "LOAD_MORE_ARTICLE"; 5 | -------------------------------------------------------------------------------- /src/app/Main.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Main from "./Main"; 4 | 5 | describe("test main", () => { 6 | it("renders without crashing", () => { 7 | const div = document.createElement("div"); 8 | ReactDOM.render(
, div); 9 | ReactDOM.unmountComponentAtNode(div); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "@babel/proposal-class-properties", 5 | "@babel/plugin-transform-runtime", 6 | "@babel/plugin-proposal-object-rest-spread", 7 | "@babel/plugin-transform-async-to-generator", 8 | "@babel/plugin-syntax-dynamic-import" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /public/assets/firebase-messaging-sw.js: -------------------------------------------------------------------------------- 1 | importScripts("https://www.gstatic.com/firebasejs/4.8.1/firebase-app.js"); 2 | importScripts("https://www.gstatic.com/firebasejs/4.8.1/firebase-messaging.js"); 3 | 4 | //eslint-disable-next-line 5 | firebase.initializeApp({ 6 | messagingSenderId: "292520951559", 7 | }); 8 | 9 | //eslint-disable-next-line 10 | const messaging = firebase.messaging(); 11 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | 3 | # These rules will change if you change your site’s custom domains or HTTPS settings 4 | 5 | # Redirect domain aliases to primary domain 6 | https://fptu.tech/* https://fuhcm.com/:splat 301! 7 | https://www.fuhcm.com/* https://fuhcm.com/:splat 301! 8 | 9 | # Optional: Redirect default Netlify subdomain to primary domain 10 | https://fptu.netlify.com/* https://fuhcm.com/:splat 301! 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | build 11 | ./build 12 | /build 13 | dist 14 | ./dist 15 | /dist 16 | 17 | # misc 18 | .DS_Store 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | # src/**/*.css 25 | # src/**/*.css.map 26 | 27 | .idea 28 | 29 | deploy.sh 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | web: 4 | build: 5 | context: . 6 | dockerfile: ./dev.Dockerfile 7 | expose: 8 | - 3001 9 | ports: 10 | - 3001:3001 11 | image: fptu-app-dev 12 | container_name: fptu-app-dev 13 | volumes: 14 | - ./public:/root/src/app/public 15 | - ./src:/root/src/app/src 16 | tty: true 17 | -------------------------------------------------------------------------------- /public/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "FUHCM.com", 3 | "name": "FPT University HCM", 4 | "icons": [ 5 | { 6 | "src": "./images/fptuhcm-confessions.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#000000" 15 | } 16 | -------------------------------------------------------------------------------- /webpack/production/webpack.server.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = () => { 4 | return require("../common/webpack.core")({ 5 | environment: "production", 6 | entry : { server: "./server/index.js" }, 7 | output : { 8 | path : path.resolve(__dirname, "../../", "dist/server"), 9 | filename: `[name].js`, 10 | }, 11 | mode : "production", 12 | target: "node", 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "endOfLine": "lf", 5 | "printWidth": 80, 6 | "useTabs": false, 7 | "arrowParens": "avoid", 8 | "bracketSpacing": true, 9 | "singleQuote": false, 10 | "jsxBracketSameLine": false, 11 | "jsxSingleQuote": false, 12 | "overrides": [ 13 | { 14 | "files": "*.{scss,sass}", 15 | "options": { 16 | "parser": "scss" 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /dev.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.17.0-alpine as builder 2 | 3 | RUN mkdir -p /root/src/app 4 | WORKDIR /root/src/app 5 | ENV PATH /root/src/app/node_modules/.bin:$PATH 6 | 7 | COPY . . 8 | 9 | RUN npm install 10 | 11 | EXPOSE 3000 12 | 13 | ENTRYPOINT ["npm","run","serve"] 14 | 15 | # This is docker build command: 16 | # docker build -f dev.Dockerfile -t fptu-app-dev . 17 | 18 | # This is docker run command: 19 | # docker run -it -p 3001:3001 -v src:/root/src/app/src fptu-app-dev:latest 20 | -------------------------------------------------------------------------------- /src/app/modules/loading/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import { Layout, Skeleton } from "antd"; 4 | 5 | const { Content } = Layout; 6 | 7 | function Loading() { 8 | return ( 9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 | ); 17 | } 18 | 19 | export default Loading; 20 | -------------------------------------------------------------------------------- /src/app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import authReducer from "./AuthReducer"; 4 | import homeReducer from "../modules/home/reducers/HomeReducer"; 5 | import mediumReducer from "../modules/medium/reducers/MediumReducer"; 6 | import codedaoReducer from "../modules/toidicodedao/reducers/CodeDaoReducer"; 7 | 8 | const reducers = combineReducers({ 9 | authReducer, 10 | homeReducer, 11 | mediumReducer, 12 | codedaoReducer, 13 | }); 14 | 15 | export default reducers; 16 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | node { 2 | def app 3 | 4 | stage('Initial') { 5 | checkout scm 6 | } 7 | 8 | stage('Build') { 9 | app = docker.build("fuhcm/fptu-app") 10 | } 11 | 12 | stage('Test') { 13 | sh 'echo "Tests passed"' 14 | } 15 | 16 | stage('Deploy') { 17 | docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-credentials') { 18 | app.push("${env.BUILD_NUMBER}") 19 | app.push("latest") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm ci 23 | npm run build --if-present 24 | env: 25 | CI: true 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2015", 5 | "declaration": false, 6 | "jsx": "react", 7 | "esModuleInterop": true, 8 | "rootDir": "./src", 9 | "outDir": "./dist", 10 | "lib": ["es6", "dom"], 11 | "strict": true, 12 | "removeComments": true, 13 | "skipLibCheck": true, 14 | "moduleResolution": "node", 15 | "noImplicitAny": false, 16 | "sourceMap": false 17 | }, 18 | "include": ["src/**/*"], 19 | "exclude": ["node_modules", "**/*.test.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.17.0 as builder 2 | 3 | RUN mkdir -p /root/src/app 4 | WORKDIR /root/src/app 5 | ENV PATH /root/src/app/node_modules/.bin:$PATH 6 | 7 | COPY package.json package-lock.json ./ 8 | RUN npm install 9 | 10 | COPY . . 11 | 12 | RUN npm run build 13 | 14 | FROM node:10.17.0-alpine 15 | 16 | WORKDIR /root/src/app 17 | 18 | COPY --from=builder /root/src/app/dist /root/src/app/dist 19 | 20 | EXPOSE 3000 21 | 22 | ENTRYPOINT ["node","./dist/server/server.js"] 23 | 24 | # This is docker build command: 25 | # docker build -t fptu-app . 26 | 27 | # This is docker run command: 28 | # docker run -d --name fptu-app -p 3001:3000 fptu-app:latest 29 | -------------------------------------------------------------------------------- /src/app/Main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { Switch, Route } from "react-router-dom"; 3 | import { loadReCaptcha } from "react-recaptcha-google"; 4 | import App from "./App"; 5 | import LandingPage from "./modules/landing-page/LandingPage"; 6 | 7 | function Main() { 8 | useEffect(() => { 9 | loadReCaptcha(); 10 | }, []); 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default Main; 23 | -------------------------------------------------------------------------------- /src/app/utils/shared/disqus/DisqusComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Disqus from "disqus-react"; 3 | 4 | class DisqusComponent extends Component { 5 | render() { 6 | if (typeof window !== "undefined") { 7 | const { guid, title } = this.props; 8 | 9 | const disqusConfig = { 10 | url : window.location.href, 11 | indentifier: guid, 12 | title : title, 13 | }; 14 | 15 | return ( 16 | 17 | ); 18 | } else { 19 | return null; 20 | } 21 | } 22 | } 23 | 24 | export default DisqusComponent; 25 | -------------------------------------------------------------------------------- /src/app/modules/toidicodedao/actions/CodeDaoActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CODEDAO_ARTICLE_LOADING, 3 | GET_CODEDAO_ARTICLE_SUCCESS, 4 | GET_CODEDAO_ARTICLE_FAILURE, 5 | } from "../types"; 6 | 7 | export const getCodedaoArticles = () => { 8 | return dispatch => { 9 | dispatch({ 10 | type: GET_CODEDAO_ARTICLE_LOADING, 11 | }); 12 | 13 | return FPTUSDK.crawl 14 | .getArticles("codedao") 15 | .then(data => { 16 | dispatch({ 17 | type : GET_CODEDAO_ARTICLE_SUCCESS, 18 | payload: data, 19 | }); 20 | }) 21 | .catch(err => { 22 | dispatch({ 23 | type : GET_CODEDAO_ARTICLE_FAILURE, 24 | payload: err, 25 | }); 26 | }); 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/modules/radio/Radio.scss: -------------------------------------------------------------------------------- 1 | iframe { 2 | max-width: 100%; 3 | 4 | @media screen and (max-width: 1024px) { 5 | height: 250px; 6 | } 7 | } 8 | 9 | .radio-wrapper { 10 | background: url("/assets/images/radio-background.jpg"); 11 | background-repeat: no-repeat; 12 | background-size: 100%; 13 | background-color: #faebca; 14 | } 15 | 16 | .selected-row { 17 | color: darkblue; 18 | font-weight: bold; 19 | font-size: 1rem; 20 | } 21 | 22 | .list-radio-inside { 23 | padding: 1rem; 24 | background-color: rgba(255, 255, 255, 0.85); 25 | border-radius: 1rem; 26 | max-height: 300px; 27 | overflow-y: scroll !important; 28 | } 29 | 30 | .ant-list-split .ant-list-item { 31 | border-bottom: none; 32 | } 33 | 34 | h2 { 35 | font-size: 2rem; 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "files.insertFinalNewline": true, 4 | "javascript.implicitProjectConfig.checkJs": true, 5 | "javascript.format.enable": false, 6 | "javascript.validate.enable": false, 7 | "typescript.validate.enable": false, 8 | "typescript.suggestionActions.enabled": false, 9 | "javascript.updateImportsOnFileMove.enabled": "always", 10 | "css.validate": false, 11 | "less.validate": false, 12 | "scss.validate": false, 13 | "stylelint.config": { 14 | "extends": "./.stylelintrc" 15 | }, 16 | "eslint.packageManager": "yarn", 17 | "files.exclude": { 18 | ".cache": true, 19 | "node_modules/": true, 20 | "**/._*": true 21 | // "**/*.css": true 22 | }, 23 | "git.autofetch": true, 24 | "git.ignoreLimitWarning": true 25 | } 26 | -------------------------------------------------------------------------------- /src/app/App.scss: -------------------------------------------------------------------------------- 1 | @import "~antd/dist/antd.css"; 2 | @import "~ant-design-pro/dist/ant-design-pro.css"; 3 | 4 | body { 5 | background-color: #f0f2f5; 6 | } 7 | 8 | .content-container { 9 | padding: 0 50px; 10 | } 11 | 12 | .content-wrapper { 13 | background: #fff; 14 | min-height: 70vh; 15 | padding: 2rem; 16 | } 17 | 18 | @media screen and (max-width: 640px) { 19 | .ant-layout-header { 20 | padding: 0; 21 | } 22 | 23 | .ant-layout-content { 24 | padding: 0; 25 | } 26 | } 27 | 28 | .ant-list { 29 | margin: 0 auto; 30 | width: 860px; 31 | 32 | @media screen and (max-width: 1024px) { 33 | width: 100%; 34 | } 35 | } 36 | 37 | pre { 38 | background: rgba(0, 0, 0, 0.05); 39 | font-size: 14px; 40 | margin-top: 0; 41 | padding: 20px; 42 | white-space: pre-wrap; 43 | } 44 | 45 | .ant-layout-header { 46 | background: #000; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/modules/medium/actions/MediumActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_MEDIUM_ARTICLE_LOADING, 3 | GET_MEDIUM_ARTICLE_SUCCESS, 4 | GET_MEDIUM_ARTICLE_FAILURE, 5 | LOAD_MORE_ARTICLE, 6 | } from "../types"; 7 | 8 | export const getMediumArticles = () => { 9 | return dispatch => { 10 | dispatch({ 11 | type: GET_MEDIUM_ARTICLE_LOADING, 12 | }); 13 | 14 | return FPTUSDK.crawl 15 | .getArticles("medium") 16 | .then(data => { 17 | dispatch({ 18 | type : GET_MEDIUM_ARTICLE_SUCCESS, 19 | payload: data, 20 | }); 21 | }) 22 | .catch(err => { 23 | dispatch({ 24 | type : GET_MEDIUM_ARTICLE_FAILURE, 25 | payload: err, 26 | }); 27 | }); 28 | }; 29 | }; 30 | 31 | export const loadMoreArticle = () => { 32 | return dispatch => { 33 | dispatch({ 34 | type: LOAD_MORE_ARTICLE, 35 | }); 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /src/app/modules/home/actions/HomeActions.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_HOME_ARTICLE_LOADING, 3 | GET_HOME_ARTICLE_SUCCESS, 4 | GET_HOME_ARTICLE_FAILURE, 5 | } from "../types"; 6 | 7 | export const getHomeArticles = (numLoad = 9) => { 8 | return async dispatch => { 9 | dispatch({ 10 | type: GET_HOME_ARTICLE_LOADING, 11 | }); 12 | 13 | try { 14 | const crawlData = await FPTUSDK.crawl.getArticles(`fpt?load=${numLoad}`); 15 | const postData = await FPTUSDK.post.list(); 16 | const postDataWithGUID = postData.map(e => ({ 17 | ...e, 18 | guid: e._id, 19 | })); 20 | 21 | dispatch({ 22 | type : GET_HOME_ARTICLE_SUCCESS, 23 | payload: postDataWithGUID.reverse().concat(crawlData.reverse()), 24 | }); 25 | } catch (err) { 26 | dispatch({ 27 | type : GET_HOME_ARTICLE_FAILURE, 28 | payload: err, 29 | }); 30 | } 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from "redux"; 2 | import { persistStore, persistReducer } from "redux-persist"; 3 | import thunk from "redux-thunk"; 4 | import logger from "redux-logger"; 5 | 6 | import storage from "redux-persist/lib/storage"; 7 | import reducers from "./app/reducers"; 8 | 9 | const initialState = {}; 10 | 11 | const persistConfig = { 12 | key: "root", 13 | storage, 14 | }; 15 | 16 | const persistedReducer = persistReducer(persistConfig, reducers); 17 | 18 | export default function configureStore() { 19 | const middlewares = [thunk]; 20 | const enhancers = 21 | APP_ENV.NODE_ENV === "production" 22 | ? [applyMiddleware(...middlewares)] 23 | : [applyMiddleware(...middlewares, logger)]; 24 | const store = createStore( 25 | persistedReducer, 26 | initialState, 27 | compose(...enhancers) 28 | ); 29 | 30 | let persistor = persistStore(store); 31 | 32 | return { store, persistor }; 33 | } 34 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from "firebase/app"; 2 | import "firebase/messaging"; 3 | 4 | export const initializeFirebase = () => { 5 | firebase.initializeApp({ 6 | storageBucket : "fuhcm-253612.appspot.com", 7 | messagingSenderId: "834798810236", 8 | }); 9 | }; 10 | 11 | export const askForPermissionToReceiveNotifications = async () => { 12 | try { 13 | const messaging = firebase.messaging(); 14 | await messaging.requestPermission(); 15 | const token = await messaging.getToken(); 16 | 17 | await FPTUSDK.push.syncPush(token); 18 | 19 | return token; 20 | } catch (err) { 21 | //eslint-disable-next-line 22 | console.log(err); 23 | return null; 24 | } 25 | }; 26 | 27 | export const initialServiceWorker = () => { 28 | if (typeof window !== "undefined") { 29 | navigator.serviceWorker 30 | .register("/assets/firebase-messaging-sw.js") 31 | .then(registration => { 32 | firebase.messaging().useServiceWorker(registration); 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/modules/home/reducers/HomeReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_HOME_ARTICLE_LOADING, 3 | GET_HOME_ARTICLE_SUCCESS, 4 | GET_HOME_ARTICLE_FAILURE, 5 | } from "../types"; 6 | 7 | const initialState = { 8 | loading: true, 9 | posts : [], 10 | error : null, 11 | }; 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case GET_HOME_ARTICLE_LOADING: 16 | return { 17 | ...state, 18 | loading: true, 19 | }; 20 | case GET_HOME_ARTICLE_SUCCESS: 21 | if (action.payload && action.payload.length) { 22 | return { 23 | ...state, 24 | loading: false, 25 | posts : action.payload, 26 | error : null, 27 | }; 28 | } 29 | 30 | return { 31 | ...state, 32 | loading: false, 33 | error : "Null list", 34 | }; 35 | case GET_HOME_ARTICLE_FAILURE: 36 | return { 37 | ...state, 38 | loading: false, 39 | error : action.payload, 40 | }; 41 | default: 42 | return state; 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 gosu-team 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. -------------------------------------------------------------------------------- /webpack/production/webpack.client.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { ReactLoadablePlugin } = require("react-loadable/webpack"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const ManifestPlugin = require("webpack-manifest-plugin"); 5 | const { commonPath } = require("../common/webpack.common"); 6 | 7 | module.exports = () => { 8 | return require("../common/webpack.core")({ 9 | environment: "production", 10 | entry : { 11 | browser: "./src/index.js", 12 | }, 13 | output: { 14 | path : path.resolve(__dirname, "../../", "dist/client"), 15 | chunkFilename: "[name].[chunkhash].js", 16 | filename : `[name].[chunkhash].js`, 17 | publicPath : "/client/", 18 | }, 19 | target : "web", 20 | mode : "production", 21 | devtool: null, 22 | plugins: [ 23 | new ReactLoadablePlugin({ 24 | filename: commonPath + "/dist/react-loadable.json", 25 | }), 26 | new MiniCssExtractPlugin({ 27 | filename: "[name].[chunkhash].css", 28 | }), 29 | new ManifestPlugin(), 30 | ], 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imh1eW5obWluaHR1ZnVAZ21haWwuY29tIiwibmlja25hbWUiOiJEYXJrbG9yZCIsImlhdCI6MTU4NDAwMTcxMCwiZXhwIjoxNTg0MDg4MTEwfQ.9FOV-LJMDuFfwFpQV3Kw1vXvK4alJnuzw9MLmA23HCg'; 4 | 5 | async function calAvgProcess(numberOfPosts, token) { 6 | const { data } = await axios.get('https://api.fuhcm.com/api/v1/confessions?load=' + numberOfPosts, { 7 | headers: { 8 | 'authorization': 'Bearer ' + token, 9 | }, 10 | }); 11 | 12 | const realData = data.filter(e => e.status !== 0); 13 | const meanStr = realData.map(item => Math.abs(new Date(item.updatedAt) - new Date(item.createdAt)) / 36e5.toFixed(2)); 14 | const meanNum = meanStr.map(e => parseFloat(e)); 15 | const sum = meanNum.reduce((a, b) => a + b, 0); 16 | return (sum / meanNum.length); 17 | } 18 | 19 | async function main() { 20 | for (let i = 0; i < 15; i++) { 21 | const index = i + 1; 22 | const avg = await calAvgProcess(500 * index, token); 23 | console.log(`Thoi gian duyet cua ${500 * index} confessions truoc: ${avg.toFixed(2)}h`); 24 | } 25 | } 26 | 27 | main(); 28 | -------------------------------------------------------------------------------- /src/app/utils/shared/auth/withAuthRouteComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Redirect } from "react-router-dom"; 3 | import LocalStorageUtils from "@utils/browser/LocalStorage"; 4 | 5 | const withAuthRouteComponent = redirectUrl => Child => 6 | class RequireAuthorizedComponent extends Component { 7 | constructor() { 8 | super(); 9 | if (typeof window !== "undefined" && typeof document !== "undefined") { 10 | // When constructing component in DOM env 11 | this.renderFn = this._renderIfAuthenticated; 12 | } else { 13 | this.renderFn = this._renderWithoutAuthenticated; 14 | } 15 | } 16 | _renderIfAuthenticated = () => { 17 | const { props } = this; 18 | if (LocalStorageUtils.isAuthenticated()) { 19 | return ; 20 | } else { 21 | return ; 22 | } 23 | }; 24 | 25 | _renderWithoutAuthenticated = () => { 26 | return ; 27 | }; 28 | 29 | render() { 30 | return this.renderFn(); 31 | } 32 | }; 33 | 34 | export default withAuthRouteComponent; 35 | -------------------------------------------------------------------------------- /webpack/development/webpack.development.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const dotenv = require("dotenv"); 3 | const webpack = require("webpack"); 4 | const htmlWebpackPlugin = require("html-webpack-plugin"); 5 | 6 | const APP_ENV = dotenv.config().error ? {} : dotenv.config().parsed; 7 | 8 | module.exports = require("../common/webpack.core")({ 9 | environment: "development", 10 | entry : { 11 | browser: "./src/index.js", 12 | }, 13 | output: { 14 | path : path.resolve(__dirname, "../../", "public"), 15 | chunkFilename: "[name].js", 16 | filename : `[name].js`, 17 | publicPath : "/", 18 | }, 19 | devtool : "source-map", 20 | devServer: { 21 | contentBase : path.join(__dirname, "../../public"), 22 | port : APP_ENV.PORT, 23 | historyApiFallback: true, 24 | hot : true, 25 | compress : false, 26 | host : "0.0.0.0", 27 | }, 28 | plugins: [ 29 | new htmlWebpackPlugin({ 30 | filename: "index.html", 31 | template: "src/index.html", 32 | }), 33 | new webpack.NamedModulesPlugin(), 34 | new webpack.HotModuleReplacementPlugin(), 35 | ], 36 | }); 37 | -------------------------------------------------------------------------------- /webpack/production/webpack.csr.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { ReactLoadablePlugin } = require("react-loadable/webpack"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const htmlWebpackPlugin = require("html-webpack-plugin"); 5 | const { commonPath } = require("../common/webpack.common"); 6 | 7 | module.exports = () => { 8 | return require("../common/webpack.core")({ 9 | environment: "production", 10 | entry : { 11 | browser: "./src/index.js", 12 | }, 13 | output: { 14 | path : path.resolve(__dirname, "../../", "dist/client"), 15 | chunkFilename: "[name].[chunkhash].js", 16 | filename : `[name].[chunkhash].js`, 17 | publicPath : "/client/", 18 | }, 19 | target : "web", 20 | mode : "production", 21 | devtool: null, 22 | plugins: [ 23 | new ReactLoadablePlugin({ 24 | filename: commonPath + "/dist/react-loadable.json", 25 | }), 26 | new MiniCssExtractPlugin({ 27 | filename: "[name].[chunkhash].css", 28 | }), 29 | new htmlWebpackPlugin({ 30 | filename: "../index.html", 31 | template: "src/index.html", 32 | }), 33 | ], 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/app/modules/not-found/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Exception from "ant-design-pro/lib/Exception"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import { Layout, Button } from "antd"; 6 | 7 | import Helmet from "react-helmet-async"; 8 | 9 | const { Content } = Layout; 10 | 11 | function NotFound() { 12 | return ( 13 | 14 | 15 | 404 - FUHCM.com 16 | 17 |
24 | 28 | 29 | 30 | )} 31 | title="404" 32 | desc="Mô phật, thí chủ đi đâu mà lạc vào đây?" 33 | /> 34 |
35 |
36 | ); 37 | } 38 | 39 | export default NotFound; 40 | -------------------------------------------------------------------------------- /src/app/modules/medium/reducers/MediumReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_MEDIUM_ARTICLE_LOADING, 3 | GET_MEDIUM_ARTICLE_SUCCESS, 4 | GET_MEDIUM_ARTICLE_FAILURE, 5 | LOAD_MORE_ARTICLE, 6 | } from "../types"; 7 | 8 | const initialState = { 9 | loading: true, 10 | posts : [], 11 | load : 9, 12 | error : null, 13 | }; 14 | 15 | export default (state = initialState, action) => { 16 | switch (action.type) { 17 | case GET_MEDIUM_ARTICLE_LOADING: 18 | return { 19 | ...state, 20 | loading: true, 21 | }; 22 | case GET_MEDIUM_ARTICLE_SUCCESS: 23 | if (action.payload && action.payload.length) { 24 | return { 25 | ...state, 26 | loading: false, 27 | posts : action.payload, 28 | error : null, 29 | }; 30 | } 31 | return { 32 | ...state, 33 | loading: false, 34 | error : "Null list", 35 | }; 36 | case GET_MEDIUM_ARTICLE_FAILURE: 37 | return { 38 | ...state, 39 | loading: false, 40 | error : action.payload, 41 | }; 42 | case LOAD_MORE_ARTICLE: 43 | return { 44 | ...state, 45 | load: state.load + 9, 46 | }; 47 | default: 48 | return state; 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/app/modules/toidicodedao/reducers/CodeDaoReducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_CODEDAO_ARTICLE_LOADING, 3 | GET_CODEDAO_ARTICLE_SUCCESS, 4 | GET_CODEDAO_ARTICLE_FAILURE, 5 | } from "../types"; 6 | 7 | const initialState = { 8 | loading: true, 9 | posts : [], 10 | error : null, 11 | }; 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case GET_CODEDAO_ARTICLE_LOADING: 16 | return { 17 | ...state, 18 | loading: true, 19 | }; 20 | case GET_CODEDAO_ARTICLE_SUCCESS: 21 | if (action.payload && action.payload.length) { 22 | // Sort posts by pubDate 23 | // action.payload.sort((left, right) => { 24 | // return moment.utc(right.pubDate).diff(moment.utc(left.pubDate)); 25 | // }); 26 | 27 | return { 28 | ...state, 29 | loading: false, 30 | posts : action.payload, 31 | error : null, 32 | }; 33 | } 34 | 35 | return { 36 | ...state, 37 | loading: false, 38 | error : "Null list", 39 | }; 40 | case GET_CODEDAO_ARTICLE_FAILURE: 41 | return { 42 | ...state, 43 | loading: false, 44 | error : action.payload, 45 | }; 46 | default: 47 | return state; 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { BrowserRouter } from "react-router-dom"; 4 | import { Provider } from "react-redux"; 5 | import Loadable from "react-loadable"; 6 | import { HelmetProvider } from "react-helmet-async"; 7 | import serverStyleCleanup from "node-style-loader/clientCleanup"; 8 | import { PersistGate } from "redux-persist/integration/react"; 9 | import Main from "./app/Main"; 10 | import { initializeFirebase, initialServiceWorker } from "./firebase"; 11 | 12 | import configureStore from "./store"; 13 | 14 | const { store, persistor } = configureStore(); 15 | 16 | const AppBundle = ( 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | ); 27 | 28 | Loadable.preloadReady().then(() => { 29 | const loadingDOM = document.getElementById("loading-bg"); 30 | if (loadingDOM) { 31 | loadingDOM.remove(); 32 | } 33 | 34 | ReactDOM.hydrate(AppBundle, document.getElementById("root")); 35 | }); 36 | 37 | // Hot reload 38 | if (module.hot) { 39 | module.hot.accept(); 40 | } 41 | 42 | serverStyleCleanup(); 43 | initializeFirebase(); 44 | initialServiceWorker(); 45 | -------------------------------------------------------------------------------- /src/app/modules/landing-page/Header.scss: -------------------------------------------------------------------------------- 1 | $header-height: 80px; 2 | 3 | #header { 4 | background-color: #fff; 5 | height: 64px; 6 | position: relative; 7 | z-index: 10; 8 | } 9 | 10 | #logo { 11 | float: left; 12 | height: 64px; 13 | line-height: 64px; 14 | overflow: hidden; 15 | padding-left: 40px; 16 | text-decoration: none; 17 | img { 18 | display: inline; 19 | margin-right: 16px; 20 | vertical-align: middle; 21 | width: 32px; 22 | } 23 | span { 24 | color: #3fa9ff; 25 | font-size: 14px; 26 | line-height: 28px; 27 | outline: none; 28 | } 29 | } 30 | 31 | .header-meta { 32 | // @include clearfix(); 33 | padding-right: 40px; 34 | } 35 | 36 | #menu { 37 | float: right; 38 | height: 64px; 39 | overflow: hidden; 40 | .ant-menu { 41 | line-height: 60px; 42 | } 43 | .ant-menu-horizontal { 44 | border-bottom: none; 45 | & > .ant-menu-item { 46 | border-top: 2px solid transparent; 47 | &:hover { 48 | border-bottom: 2px solid transparent; 49 | border-top: 2px solid #3fa9ff; 50 | } 51 | } 52 | & > .ant-menu-item-selected { 53 | border-bottom: 2px solid transparent; 54 | border-top: 2px solid #3fa9ff; 55 | a { 56 | color: #3fa9ff; 57 | } 58 | } 59 | } 60 | } 61 | 62 | #preview { 63 | float: right; 64 | margin-left: 32px; 65 | padding-top: 17px; 66 | button { 67 | border-radius: 32px; 68 | } 69 | } 70 | 71 | #preview-button { 72 | .ant-btn { 73 | color: #3fa9ff; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## FPTU Tech Insights — "[isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/)" web app   2 | 3 | [fptu-app](https://fuhcm.com) is an isomorphic web app built on top of [Node.js](https://nodejs.org/), 4 | [Express](http://expressjs.com/) and [React](https://facebook.github.io/react/), containing modern web development 5 | tools such as [Webpack](http://webpack.github.io/) and [Babel](http://babeljs.io/). 6 | 7 | The webpack configuration now support both Babel and TypeScript, so you can have both `.js` and `.ts` files in the source code. 8 | 9 | ## Environment 10 | 11 | Assuming you have a working `DockerCE`. 12 | 13 | ## Development 14 | 15 | Run with develop container: 16 | 17 | `$ docker-compose up` 18 | 19 | ## Production 20 | 21 | Build production image: 22 | 23 | `$ docker build -t fptu-app .` 24 | 25 | Run production container: 26 | 27 | `$ docker run -d --name fptu-app -p 3001:3000 fptu-app:latest` 28 | 29 | ## License 30 | 31 | Copyright © 2018-present **Huynh Minh Tu**. This source code is licensed under the MIT 32 | license found in the [LICENSE.txt](https://github.com/gosu-team/fptu-app/blob/master/LICENSE.txt) 33 | file. The documentation to the project is licensed under the 34 | [CC BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/) license. 35 | 36 | Made with ♥ by Huynh Minh Tu ([https://mrhmt.com](mrhmt.com)) 37 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "stylelint-scss", 4 | "stylelint-order", 5 | "stylelint-no-unsupported-browser-features", 6 | "stylelint-declaration-use-variable", 7 | "stylelint-declaration-strict-value", 8 | "stylelint-at-rule-no-children", 9 | "stylelint-declaration-block-no-ignored-properties", 10 | "stylelint-value-no-unknown-custom-properties", 11 | "stylelint-z-index-value-constraint", 12 | "stylelint-selector-no-utility", 13 | "stylelint-prettier" 14 | ], 15 | "extends": [ 16 | "stylelint-config-sass-guidelines", 17 | "stylelint-config-css-modules", 18 | "stylelint-prettier/recommended" 19 | ], 20 | "rules": { 21 | "prettier/prettier": true, 22 | "sh-waqar/declaration-use-variable": [["color", "font-family"]], 23 | "scale-unlimited/declaration-strict-value": [["color", "font-family"]], 24 | "plugin/declaration-block-no-ignored-properties": true, 25 | "plugin/no-unsupported-browser-features": [ 26 | true, 27 | { 28 | "severity": "warning" 29 | } 30 | ], 31 | "csstools/value-no-unknown-custom-properties": true, 32 | "plugin/z-index-value-constraint": { 33 | "min": 0, 34 | "max": 10 35 | }, 36 | "primer/selector-no-utility": true, 37 | "at-rule-no-unknown": [ 38 | true, 39 | { 40 | "ignoreAtRules": ["function", "if", "each", "include", "mixin"] 41 | } 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/modules/footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | 3 | import { withRouter } from "react-router-dom"; 4 | 5 | import { Layout, Tooltip } from "antd"; 6 | 7 | const { Footer } = Layout; 8 | 9 | type Props = { 10 | location: Location; 11 | }; 12 | 13 | type Location = { 14 | pathname: String; 15 | }; 16 | 17 | class FooterPage extends PureComponent { 18 | render() { 19 | const { location } = this.props; 20 | const isRadio: boolean = location.pathname === "/radio" ? true : false; 21 | 22 | return ( 23 | 46 | ); 47 | } 48 | } 49 | 50 | export default withRouter(FooterPage); 51 | -------------------------------------------------------------------------------- /public/loader.css: -------------------------------------------------------------------------------- 1 | #loading-bg { 2 | width: 100%; 3 | height: 100%; 4 | background: #000; 5 | display: block; 6 | position: absolute; 7 | } 8 | .loading-logo { 9 | position: absolute; 10 | left: calc(50% - 100px); 11 | top: 40%; 12 | } 13 | .loading-logo img { 14 | width: 200px; 15 | } 16 | .loading { 17 | position: absolute; 18 | left: calc(50% - 35px); 19 | top: 50%; 20 | width: 55px; 21 | height: 55px; 22 | border-radius: 50%; 23 | -webkit-box-sizing: border-box; 24 | box-sizing: border-box; 25 | border: 3px solid transparent; 26 | } 27 | .loading .effect-1, 28 | .loading .effect-2 { 29 | position: absolute; 30 | width: 100%; 31 | height: 100%; 32 | border: 3px solid transparent; 33 | border-left: 3px solid #fff; 34 | border-radius: 50%; 35 | -webkit-box-sizing: border-box; 36 | box-sizing: border-box; 37 | } 38 | 39 | .loading .effect-1 { 40 | animation: rotate 1s ease infinite; 41 | } 42 | .loading .effect-2 { 43 | animation: rotateOpacity 1s ease infinite 0.1s; 44 | } 45 | .loading .effect-3 { 46 | position: absolute; 47 | width: 100%; 48 | height: 100%; 49 | border: 3px solid transparent; 50 | border-left: 3px solid #fff; 51 | -webkit-animation: rotateOpacity 1s ease infinite 0.2s; 52 | animation: rotateOpacity 1s ease infinite 0.2s; 53 | border-radius: 50%; 54 | -webkit-box-sizing: border-box; 55 | box-sizing: border-box; 56 | } 57 | 58 | .loading .effects { 59 | transition: all 0.3s ease; 60 | } 61 | 62 | @keyframes rotate { 63 | 0% { 64 | -webkit-transform: rotate(0deg); 65 | transform: rotate(0deg); 66 | } 67 | 100% { 68 | -webkit-transform: rotate(1turn); 69 | transform: rotate(1turn); 70 | } 71 | } 72 | @keyframes rotateOpacity { 73 | 0% { 74 | -webkit-transform: rotate(0deg); 75 | transform: rotate(0deg); 76 | opacity: 0.1; 77 | } 78 | 100% { 79 | -webkit-transform: rotate(1turn); 80 | transform: rotate(1turn); 81 | opacity: 1; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["react-app", "airbnb", "eslint:recommended"], 3 | "parser": "babel-eslint", 4 | "plugins": ["react"], 5 | "rules": { 6 | "import/no-unresolved": "off", 7 | "react/jsx-filename-extension": [ 8 | "error", 9 | { 10 | "extensions": [".js", ".jsx", ".ts"] 11 | } 12 | ], 13 | "semi": ["error", "always"], 14 | "jsx-a11y/href-no-hash": ["off"], 15 | "import/prefer-default-export": "off", 16 | "react/prefer-stateless-function": "off", 17 | "import/no-extraneous-dependencies": "off", 18 | "key-spacing": [ 19 | 1, 20 | { 21 | "singleLine": { 22 | "beforeColon": false, 23 | "afterColon": true 24 | }, 25 | "multiLine": { 26 | "align": "colon", 27 | "mode": "minimum" 28 | } 29 | } 30 | ], 31 | "comma-dangle": [ 32 | "error", 33 | "always-multiline", 34 | { 35 | "functions": "never" 36 | } 37 | ], 38 | "react/jsx-indent": [1, 2], 39 | "react/jsx-indent-props": [1, 2], 40 | "react/prop-types": 0 41 | }, 42 | "settings": { 43 | "import/resolver": { 44 | "node": { 45 | "extensions": [".js", ".jsx", ".json", ".ts", ".tsx"], 46 | "paths": ["src"] 47 | }, 48 | "alias": [["browser", "./src/app/utils/"]] 49 | } 50 | }, 51 | "env": { 52 | "es6": true, 53 | "browser": true, 54 | "node": true, 55 | "worker": true, 56 | "serviceworker": true 57 | }, 58 | "parserOptions": { 59 | "ecmaVersion": 6, 60 | "sourceType": "module", 61 | "ecmaFeatures": { 62 | "jsx": true 63 | } 64 | }, 65 | "globals": { 66 | "APP_ENV": true, 67 | "ENVIRONMENT": true, 68 | "FPTUSDK": true 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/modules/error/Error.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import Exception from "ant-design-pro/lib/Exception"; 3 | import { Link } from "react-router-dom"; 4 | 5 | import { Layout, Button, message } from "antd"; 6 | 7 | import Helmet from "react-helmet-async"; 8 | 9 | const { Content } = Layout; 10 | 11 | type Props = { 12 | error: string; 13 | }; 14 | 15 | class Error extends Component { 16 | componentDidMount() { 17 | const { error } = this.props; 18 | 19 | if ( 20 | error === "ReferenceError: FPTUSDK is not defined" || 21 | error === "ReferenceError: Can't find variable: FPTUSDK" 22 | ) { 23 | message.error("Lỗi bất ngờ xảy ra, chúng tôi sẽ reload lại app!"); 24 | setTimeout(() => { 25 | window.location.reload(); 26 | }, 1000); 27 | } 28 | } 29 | 30 | report = (): void => { 31 | message.info("Đã báo công an!"); 32 | }; 33 | 34 | render() { 35 | let { error } = this.props; 36 | 37 | if ( 38 | error === "ReferenceError: FPTUSDK is not defined" || 39 | error === "ReferenceError: Can't find variable: FPTUSDK" 40 | ) { 41 | error = "Mạng không ổn định, tải thiếu thành phần ứng dụng"; 42 | } 43 | 44 | const msg: string = 45 | error === "TypeError: grecaptcha.render is not a function" 46 | ? "Lỗi load captcha, bạn reload lại trang là được nhé!" 47 | : `Đệt, lỗi "${error}"`; 48 | 49 | return ( 50 | 51 | 52 | 500 - FUHCM.com 53 | 54 |
61 | 65 | 68 | 69 | } 70 | title="500" 71 | desc={msg} 72 | /> 73 |
74 |
75 | ); 76 | } 77 | } 78 | 79 | export default Error; 80 | -------------------------------------------------------------------------------- /webpack/common/webpack.core.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const dotenv = require("dotenv"); 4 | const { 5 | nodeStyleLoader, 6 | babelLoader, 7 | typeLoader, 8 | fileLoader, 9 | styleLoader, 10 | resolve, 11 | optimization, 12 | } = require("./webpack.common"); 13 | 14 | module.exports = options => { 15 | // Select correct env file 16 | const envParse = 17 | options.environment === "production" 18 | ? dotenv.config({ 19 | path: path.resolve(process.cwd(), ".env.production"), 20 | }) 21 | : dotenv.config(); 22 | 23 | const APP_ENV = envParse.error ? {} : envParse.parsed; 24 | 25 | return { 26 | entry : options.entry, 27 | output: options.output, 28 | module: { 29 | rules: [ 30 | babelLoader, 31 | typeLoader, 32 | options.target === "node" 33 | ? {} 34 | : fileLoader(options.environment === "development"), 35 | options.target === "node" 36 | ? nodeStyleLoader 37 | : styleLoader(options.environment === "development"), 38 | ], 39 | exprContextCritical: false, 40 | }, 41 | devServer : options.devServer || {}, 42 | resolve, 43 | target : options.target || "web", 44 | externals : options.externals || [], 45 | mode : options.mode || "development", 46 | devtool : options.devtool || "", 47 | optimization: options.target === "node" ? {} : optimization, 48 | plugins : 49 | options.target === "node" 50 | ? options.plugins || [] 51 | : (options.plugins || []).concat([ 52 | new webpack.DefinePlugin({ 53 | "process.env": { 54 | NODE_ENV: JSON.stringify( 55 | APP_ENV.NODE_ENV !== "development" 56 | ? "production" 57 | : "development" 58 | ), 59 | BROWSER: JSON.stringify( 60 | options.target === "node" ? false : true 61 | ), 62 | }, 63 | APP_ENV : JSON.stringify(APP_ENV), 64 | ENVIRONMENT: JSON.stringify(options.environment), 65 | }), 66 | ]), 67 | }; 68 | }; 69 | -------------------------------------------------------------------------------- /webpack/common/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 5 | 6 | const commonPath = path.resolve(__dirname, "../../"); 7 | 8 | module.exports.babelLoader = { 9 | test : /\.(js|jsx)$/, 10 | exclude: /node_modules/, 11 | loader : "babel-loader", 12 | }; 13 | 14 | module.exports.typeLoader = { 15 | test : /\.(ts|tsx)?$/, 16 | loader: "ts-loader", 17 | }; 18 | 19 | module.exports.fileLoader = isDev => { 20 | return { 21 | test: /\.(png|jpg|gif|ttf|eot|woff|woff2|tcc|svg|otf)$/i, 22 | use : [ 23 | { 24 | loader : "url-loader", 25 | options: { 26 | limit : 4000, 27 | fallback : "file-loader", 28 | quality : 85, 29 | publicPath: isDev ? "/" : "/client/", 30 | }, 31 | }, 32 | ], 33 | }; 34 | }; 35 | 36 | module.exports.styleLoader = isDev => { 37 | return { 38 | test: /\.(s*)css$/, 39 | use : [ 40 | isDev ? "style-loader" : MiniCssExtractPlugin.loader, 41 | "css-loader", 42 | { 43 | loader: "sass-loader", 44 | }, 45 | ], 46 | }; 47 | }; 48 | 49 | module.exports.nodeStyleLoader = { 50 | test: /\.(s*)css$/, 51 | use : [ 52 | "node-style-loader", 53 | "css-loader", 54 | { 55 | loader: "sass-loader", 56 | }, 57 | ], 58 | }; 59 | 60 | module.exports.resolve = { 61 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".css", ".scss"], 62 | alias : { 63 | "@utils": commonPath + "/src/app/utils/", 64 | }, 65 | }; 66 | 67 | module.exports.optimization = { 68 | splitChunks: { 69 | chunks : "async", 70 | minSize : 30000, 71 | maxSize : 0, 72 | cacheGroups: { 73 | styles: { 74 | name : "styles", 75 | test : /\.css$/, 76 | chunks : "all", 77 | enforce: true, 78 | }, 79 | commons: { 80 | test : /[\\/]node_modules[\\/]/, 81 | name : "vendors", 82 | chunks: "all", 83 | }, 84 | }, 85 | }, 86 | minimizer: [ 87 | new TerserPlugin({ 88 | cache : true, 89 | parallel: true, 90 | }), 91 | new OptimizeCSSAssetsPlugin({}), 92 | ], 93 | }; 94 | 95 | module.exports.commonPath = commonPath; 96 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import Loadable from "react-loadable"; 2 | import * as express from "express"; 3 | import bodyParser from "body-parser"; 4 | import compression from "compression"; 5 | import morgan from "morgan"; 6 | import thunkMiddleware from "redux-thunk"; 7 | import { applyMiddleware, compose, createStore } from "redux"; 8 | import Renderer from "./render"; 9 | import state from "../src/app/reducers"; 10 | 11 | class ServerSideRendering { 12 | static PORT = 3000; // Default port for server 13 | 14 | constructor() { 15 | this.port = process.env.PORT || ServerSideRendering.PORT; 16 | this.app = express(); 17 | this.serverStore = {}; 18 | this.storeConfig(); 19 | this.createConfigMiddleWare(); 20 | this.createRouter(); 21 | this.startService(); 22 | } 23 | 24 | storeConfig = () => { 25 | const middlewares = [thunkMiddleware]; 26 | const enhancers = [applyMiddleware(...middlewares)]; 27 | const composedEnhancer = compose(...enhancers); 28 | this.serverStore = createStore(state, {}, composedEnhancer); 29 | }; 30 | 31 | createConfigMiddleWare = () => { 32 | this.app.use(compression()); 33 | this.app.use(bodyParser.json()); 34 | 35 | this.app.use( 36 | morgan("combined", { 37 | skip: function(req, res) { 38 | return res.statusCode === 200 || res.statusCode === 304; 39 | }, 40 | }) 41 | ); 42 | 43 | this.app.use(function(req, res, next) { 44 | res.header("Access-Control-Allow-Origin", "*"); 45 | res.header( 46 | "Access-Control-Allow-Headers", 47 | "Origin, X-Requested-With, Content-Type, Accept" 48 | ); 49 | 50 | next(); 51 | }); 52 | 53 | this.app.use( 54 | express.static("dist", { 55 | maxAge: 86400, 56 | }) 57 | ); 58 | 59 | // For service worker 60 | this.app.use("/sw.js", express.static("dist/sw.js", { maxAge: 86400 })); 61 | 62 | this.app.use( 63 | "/assets", 64 | express.static("dist/assets", { 65 | maxAge: 86400, 66 | }) 67 | ); 68 | }; 69 | 70 | createRouter = () => { 71 | new Renderer(this.app, this.serverStore); 72 | }; 73 | 74 | startService = () => { 75 | Loadable.preloadAll().then(() => { 76 | this.app.listen(this.port, () => { 77 | //eslint-disable-next-line 78 | console.log(`App listening on port ${this.port}!`); 79 | }); 80 | }); 81 | }; 82 | } 83 | 84 | new ServerSideRendering(); 85 | -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | importScripts( 3 | "https://storage.googleapis.com/workbox-cdn/releases/4.1.1/workbox-sw.js" 4 | ); 5 | 6 | const isPWAEnabled = false; 7 | 8 | if (workbox && isPWAEnabled) { 9 | // Force production builds 10 | workbox.setConfig({ debug: false }); 11 | 12 | workbox.routing.registerRoute( 13 | "/", 14 | new workbox.strategies.StaleWhileRevalidate() 15 | ); 16 | workbox.routing.registerRoute( 17 | "/home", 18 | new workbox.strategies.StaleWhileRevalidate() 19 | ); 20 | workbox.routing.registerRoute( 21 | "/send", 22 | new workbox.strategies.StaleWhileRevalidate() 23 | ); 24 | workbox.routing.registerRoute( 25 | "/my-confess", 26 | new workbox.strategies.StaleWhileRevalidate() 27 | ); 28 | workbox.routing.registerRoute( 29 | "/search", 30 | new workbox.strategies.StaleWhileRevalidate() 31 | ); 32 | workbox.routing.registerRoute( 33 | "/login", 34 | new workbox.strategies.StaleWhileRevalidate() 35 | ); 36 | workbox.routing.registerRoute( 37 | "/medium", 38 | new workbox.strategies.StaleWhileRevalidate() 39 | ); 40 | workbox.routing.registerRoute( 41 | "/toidicodedao", 42 | new workbox.strategies.StaleWhileRevalidate() 43 | ); 44 | workbox.routing.registerRoute( 45 | "/radio", 46 | new workbox.strategies.StaleWhileRevalidate() 47 | ); 48 | workbox.routing.registerRoute( 49 | new RegExp(".+\\.js$"), 50 | new workbox.strategies.StaleWhileRevalidate() 51 | ); 52 | workbox.routing.registerRoute( 53 | new RegExp(".+\\.css$"), 54 | new workbox.strategies.StaleWhileRevalidate() 55 | ); 56 | workbox.routing.registerRoute( 57 | new RegExp(".+\\.jpg$"), 58 | new workbox.strategies.StaleWhileRevalidate() 59 | ); 60 | workbox.routing.registerRoute( 61 | new RegExp(".+\\.png$"), 62 | new workbox.strategies.StaleWhileRevalidate() 63 | ); 64 | workbox.routing.registerRoute( 65 | new RegExp(".+\\.ico$"), 66 | new workbox.strategies.StaleWhileRevalidate() 67 | ); 68 | workbox.routing.registerRoute( 69 | new RegExp(".+\\.json$"), 70 | new workbox.strategies.StaleWhileRevalidate() 71 | ); 72 | workbox.routing.registerRoute( 73 | new RegExp(".+\\.woff2$"), 74 | new workbox.strategies.StaleWhileRevalidate() 75 | ); 76 | } else { 77 | console.log(`Boo! Workbox didn't load 😬`); 78 | } 79 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./App.scss"; 3 | import { Route, Switch, Link, withRouter } from "react-router-dom"; 4 | 5 | import { Layout } from "antd"; 6 | 7 | import HeaderPage from "./modules/header/Header"; 8 | import FooterPage from "./modules/footer/Footer"; 9 | import Routes from "./Routes"; 10 | import NotFound from "./modules/not-found/NotFound"; 11 | import Error from "./modules/error/Error"; 12 | 13 | type Props = { 14 | location: Location; 15 | }; 16 | 17 | type State = { 18 | hasError: boolean; 19 | errorText: string; 20 | }; 21 | 22 | type Location = { 23 | pathname: string; 24 | }; 25 | 26 | class App extends Component { 27 | state = { 28 | hasError: false, 29 | errorText: "", 30 | }; 31 | 32 | static getDerivedStateFromError(error): State { 33 | return { 34 | hasError: true, 35 | errorText: error.toString(), 36 | }; 37 | } 38 | 39 | render() { 40 | const { location } = this.props; 41 | const { hasError, errorText } = this.state; 42 | const isRadio = location.pathname === "/radio" ? true : false; 43 | 44 | return ( 45 | 46 |
53 | {!isRadio && ( 54 | 55 | FUHCM.com 60 | 61 | )} 62 | {isRadio && ( 63 | 64 | FUHCM.com 69 | 70 | )} 71 |
72 | 73 | {hasError && } 74 | {!hasError && ( 75 | 76 | {Routes.map(route => { 77 | return ( 78 | 84 | ); 85 | })} 86 | 87 | 88 | )} 89 | 90 |
91 | ); 92 | } 93 | } 94 | 95 | export default withRouter(App); 96 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | FUHCM.com 31 | 32 | 33 | 34 |
35 | 38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | 53 | 54 | 55 | 56 | 63 | 64 | 68 | 77 | 78 | -------------------------------------------------------------------------------- /src/app/utils/browser/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import uuidv4 from "uuid/v4"; 2 | 3 | export const LOCAL_STORAGE_KEY = { 4 | JWT: "cfapp_jwt", 5 | EMAIL: "cfapp_email", 6 | NICKNAME: "cfapp_nickname", 7 | SENDER: "cfapp_sendertoken", 8 | NOTIFICATION: "cfapp_notification_v1", 9 | PUSH_ID: "cfapp_push_id", 10 | }; 11 | 12 | class LocalStorageUtils { 13 | public getItem(key: string, defaultValue: any): any { 14 | if (typeof localStorage !== "undefined") { 15 | return localStorage.getItem(key) || defaultValue; 16 | } 17 | 18 | return "undefined"; 19 | } 20 | 21 | public setItem(key: string, value: any): void { 22 | if (typeof localStorage !== "undefined") { 23 | localStorage.setItem(key, value); 24 | } 25 | } 26 | 27 | public removeItem(key: string): void { 28 | if (typeof localStorage !== "undefined") { 29 | localStorage.removeItem(key); 30 | } 31 | } 32 | 33 | public clear(): void { 34 | if (typeof localStorage !== "undefined") { 35 | localStorage.clear(); 36 | } 37 | } 38 | 39 | public isAuthenticated(): boolean | string { 40 | const jwt: string = this.getItem(LOCAL_STORAGE_KEY.JWT, ""); 41 | return jwt && jwt !== "undefined"; 42 | } 43 | 44 | public getJWT(): string { 45 | return this.getItem(LOCAL_STORAGE_KEY.JWT, ""); 46 | } 47 | 48 | public getEmail(): string { 49 | return this.getItem(LOCAL_STORAGE_KEY.EMAIL, ""); 50 | } 51 | 52 | public getName(): string { 53 | const email = this.getItem(LOCAL_STORAGE_KEY.EMAIL, ""); 54 | 55 | return email.substring(0, email.lastIndexOf("@")); 56 | } 57 | 58 | public getNickName(): string { 59 | return this.getItem(LOCAL_STORAGE_KEY.NICKNAME, ""); 60 | } 61 | 62 | public generateSenderToken(): void { 63 | const token = this.getItem(LOCAL_STORAGE_KEY.SENDER, ""); 64 | 65 | if (!token || token === "undefined") { 66 | const newSenderToken: string = uuidv4(); 67 | 68 | this.setItem(LOCAL_STORAGE_KEY.SENDER, newSenderToken); 69 | } 70 | } 71 | 72 | public getSenderToken(): string { 73 | this.generateSenderToken(); 74 | 75 | return this.getItem(LOCAL_STORAGE_KEY.SENDER, "guest"); 76 | } 77 | 78 | public syncPush(id: string): void { 79 | this.setItem(LOCAL_STORAGE_KEY.PUSH_ID, id); 80 | } 81 | 82 | public getPushID(): string { 83 | return this.getItem(LOCAL_STORAGE_KEY.PUSH_ID, ""); 84 | } 85 | 86 | public setNotificationLoaded(): void { 87 | this.setItem(LOCAL_STORAGE_KEY.NOTIFICATION, true); 88 | } 89 | 90 | public isNotificationLoaded(): boolean | string { 91 | const loaded: string = this.getItem(LOCAL_STORAGE_KEY.NOTIFICATION, ""); 92 | return loaded && loaded !== "undefined"; 93 | } 94 | } 95 | 96 | export default new LocalStorageUtils(); 97 | -------------------------------------------------------------------------------- /src/app/modules/landing-page/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./Header.scss"; 3 | import { Link } from "react-router-dom"; 4 | import { Row, Col, Icon, Menu, Button, Popover } from "antd"; 5 | 6 | import { enquireScreen } from "enquire-js"; 7 | 8 | class Header extends React.Component { 9 | state = { 10 | menuVisible: false, 11 | menuMode : "horizontal", 12 | }; 13 | 14 | componentDidMount() { 15 | enquireScreen(b => { 16 | this.setState({ menuMode: b ? "inline" : "horizontal" }); 17 | }); 18 | } 19 | 20 | onMenuVisibleChange = visible => { 21 | this.setState({ menuVisible: visible }); 22 | }; 23 | 24 | render() { 25 | const { menuMode, menuVisible } = this.state; 26 | 27 | const menu = ( 28 | 29 | 30 | Trang chính 31 | 32 | 33 | 34 | Thư viện Confessions 35 | 36 | 37 | 38 | 39 | Radio 40 | 41 | 42 | {menuMode === "inline" && ( 43 | 44 | 49 | Facebook Fan Page 50 | 51 | 52 | )} 53 | 54 | ); 55 | 56 | return ( 57 | 92 | ); 93 | } 94 | } 95 | 96 | export default Header; 97 | -------------------------------------------------------------------------------- /server/engine/index.js: -------------------------------------------------------------------------------- 1 | //eslint-disable-next-line 2 | const manifest = require("../../dist/client/manifest.json"); 3 | 4 | const vendorCss = [manifest["vendors.css"], manifest["browser.css"]]; 5 | const vendorJs = [manifest["vendors.js"], manifest["browser.js"]]; 6 | 7 | export default ({ html, preState, helmet, bundles }) => { 8 | let styles = bundles.filter(bundle => bundle.file.endsWith(".css")); 9 | let scripts = bundles.filter(bundle => bundle.file.endsWith(".js")); 10 | 11 | return ` 12 | 13 | 14 | 15 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | ${helmet.title.toString()} 31 | ${helmet.link.toString()} 32 | ${helmet.meta.toString()} 33 | ${vendorCss 34 | .map(style => { 35 | return ``; 36 | }) 37 | .join("\n")} 38 | ${styles 39 | .map(style => { 40 | return ``; 41 | }) 42 | .join("\n")} 43 | 44 | 45 | 46 |
${html}
47 | 53 | 54 | 60 | 61 | ${scripts 62 | .map(script => { 63 | return ``; 64 | }) 65 | .join("\n")} 66 | ${vendorJs 67 | .map(style => { 68 | return ``; 69 | }) 70 | .join("\n")} 71 | 78 | 82 | 91 | 92 | `; 93 | }; 94 | -------------------------------------------------------------------------------- /server/render/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { renderToString } from "react-dom/server"; 3 | import { StaticRouter } from "react-router-dom"; 4 | import { Provider } from "react-redux"; 5 | import { HelmetProvider } from "react-helmet-async"; 6 | import Loadable from "react-loadable"; 7 | import { getBundles } from "react-loadable/webpack"; 8 | import axios from "axios"; 9 | import striptags from "striptags"; 10 | import Engine from "../engine"; 11 | import Main from "../../src/app/Main"; 12 | 13 | //eslint-disable-next-line 14 | const stats = require("../../dist/react-loadable.json"); 15 | 16 | class Renderer { 17 | constructor(app, store) { 18 | app.get("*", this.renderApp(store)); 19 | } 20 | 21 | renderApp = store => async (req, res) => { 22 | let context = {}; 23 | let helmetContext = {}; 24 | let modules = []; 25 | 26 | const html = renderToString( 27 | modules.push(moduleName)}> 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | ); 37 | 38 | const preState = store.getState(); 39 | let bundles = getBundles(stats, modules); 40 | 41 | if ( 42 | req.originalUrl.includes("/fpt/") || 43 | req.originalUrl.includes("/medium/") || 44 | req.originalUrl.includes("/toidicodedao/") 45 | ) { 46 | const feedName = 47 | req.originalUrl.split("/")[1] === "toidicodedao" 48 | ? "codedao" 49 | : req.originalUrl.split("/")[1]; 50 | const articleID = 51 | feedName === "codedao" 52 | ? req.originalUrl.split("/")[3] 53 | : req.originalUrl.split("/")[2]; 54 | 55 | try { 56 | const data = await getData(feedName, articleID); 57 | 58 | const shortDesc = data.description 59 | ? striptags(data.description) 60 | .trim() 61 | .substring(0, 250) + "..." 62 | : "FPT University Tech Insights, How much does culture influence creative thinking?"; 63 | 64 | helmetContext.helmet.title = { 65 | toComponent: function() { 66 | return null; 67 | }, 68 | toString: function() { 69 | return `${data.title || 70 | "FPTU dot Tech"}`; 71 | }, 72 | }; 73 | 74 | helmetContext.helmet.meta = { 75 | toComponent: function() { 76 | return null; 77 | }, 78 | toString: function() { 79 | return ``; 81 | }, 82 | }; 83 | } catch (err) { 84 | // Log error 85 | } 86 | } 87 | 88 | res.send( 89 | Engine({ 90 | html, 91 | preState, 92 | helmet: helmetContext.helmet, 93 | bundles, 94 | }) 95 | ); 96 | }; 97 | } 98 | 99 | async function getData(feedName, articleID) { 100 | if (feedName === "fpt" && articleID.length === 24) { 101 | const { data } = await axios.get( 102 | "https://api.fuhcm.com/api/v1/posts/" + articleID 103 | ); 104 | 105 | return data; 106 | } else { 107 | const { data } = await axios.get( 108 | "https://api.fuhcm.com/api/v1/crawl/" + feedName + "/" + articleID 109 | ); 110 | 111 | return data; 112 | } 113 | } 114 | 115 | export default Renderer; 116 | -------------------------------------------------------------------------------- /src/app/modules/toidicodedao/Post.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | 3 | import { Layout, Button, Icon, BackTop, Tag, Skeleton } from "antd"; 4 | import { Link } from "react-router-dom"; 5 | import Helmet from "react-helmet-async"; 6 | import styled from "styled-components"; 7 | import NotFound from "../not-found/NotFound"; 8 | import DisqusComponent from "../../utils/shared/disqus/DisqusComponent"; 9 | 10 | const { Content } = Layout; 11 | 12 | const PostBody = styled.div` 13 | width: 740px; 14 | margin: 0 auto; 15 | 16 | img { 17 | width: unset; 18 | height: unset; 19 | max-width: 100%; 20 | margin-bottom: 1rem; 21 | } 22 | 23 | @media screen and (max-width: 1024px) { 24 | width: 100%; 25 | } 26 | `; 27 | 28 | const PostTitle = styled.h2` 29 | font-size: 2.5rem; 30 | font-weight: bold; 31 | 32 | @media screen and (max-width: 1024px) { 33 | font-size: 1.5rem; 34 | } 35 | `; 36 | 37 | const PostTag = styled.div` 38 | margin-bottom: 1rem; 39 | `; 40 | 41 | const PostContent = styled.div` 42 | font-size: 1.25rem; 43 | font-weight: 100; 44 | 45 | @media screen and (max-width: 1024px) { 46 | font-size: 1rem; 47 | } 48 | 49 | a { 50 | color: #000; 51 | } 52 | `; 53 | 54 | class Post extends Component { 55 | constructor(props) { 56 | super(props); 57 | 58 | this.state = { 59 | loading: true, 60 | post : { 61 | title: props.location.postTitle, 62 | }, 63 | }; 64 | } 65 | 66 | componentDidMount() { 67 | const { match } = this.props; 68 | const guid = match.params.id; 69 | 70 | // Scroll to top 71 | if (typeof window !== "undefined") { 72 | window.scrollTo(0, 0); 73 | } 74 | 75 | FPTUSDK.crawl 76 | .getArticleDetails("codedao", guid) 77 | .then(data => { 78 | this.setState({ 79 | loading: false, 80 | post : data, 81 | }); 82 | }) 83 | .catch(() => { 84 | this.setState({ 85 | loading: false, 86 | post : null, 87 | }); 88 | }); 89 | } 90 | 91 | render() { 92 | const { post, loading } = this.state; 93 | 94 | if (!post) { 95 | return ; 96 | } 97 | 98 | return ( 99 | 100 | 101 | {(post && post.title) || "Medium for Devs - FUHCM.com"} 102 | 103 | 104 |
105 | 106 | 115 | 116 | 117 | {post && post.title} 118 | 119 | 124 | 125 | 126 | {post && 127 | post.categories && 128 | post.categories.length && 129 | post.categories.map(item => { 130 | return ( 131 | 136 | {item.charAt(0).toLowerCase() + item.slice(1)} 137 | 138 | ); 139 | })} 140 | 141 | 142 | 147 | 148 | 154 | 155 | 159 | 160 |
161 |
162 | ); 163 | } 164 | } 165 | 166 | export default Post; 167 | -------------------------------------------------------------------------------- /src/app/modules/new-post/NewPost.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | 4 | import { Layout, Input, Button, Skeleton, message } from "antd"; 5 | 6 | import Helmet from "react-helmet-async"; 7 | 8 | const { Content } = Layout; 9 | const { TextArea } = Input; 10 | 11 | function isImageURL(url) { 12 | return url.match(/\.(jpeg|jpg|gif|png)$/) != null; 13 | } 14 | 15 | function NewPost(props) { 16 | const [loading, setLoading] = useState(false); 17 | const [title, setTitle] = useState(null); 18 | const [categories, setCategories] = useState(null); 19 | const [thumbnail, setThumbnail] = useState(null); 20 | const [description, setDescription] = useState(null); 21 | const [content, setContent] = useState(null); 22 | const [isEdit, setIsEdit] = useState(false); 23 | 24 | useEffect(() => { 25 | const id = props.match.params.id; 26 | if (id && id.length === 24) { 27 | setIsEdit(true); 28 | setLoading(true); 29 | FPTUSDK.post 30 | .get(id) 31 | .then(data => { 32 | setTitle(data.title); 33 | setCategories(data.categories); 34 | setThumbnail(data.thumbnail); 35 | setDescription(data.description); 36 | setContent(data.content); 37 | setLoading(false); 38 | }) 39 | .catch(() => props.history.push("/404")); 40 | } 41 | }, []); 42 | 43 | function handleSend() { 44 | setLoading(true); 45 | if (!isEdit) { 46 | FPTUSDK.post 47 | .new({ 48 | title, 49 | categories, 50 | thumbnail, 51 | description, 52 | content, 53 | }) 54 | .then(data => { 55 | message.success("Đã gửi bài viết mới"); 56 | props.history.push(`/fpt/${data.id}`); 57 | }) 58 | .catch(() => { 59 | message.error("Có lỗi xảy ra"); 60 | }) 61 | .finally(() => setLoading(false)); 62 | } else { 63 | FPTUSDK.post 64 | .update(props.match.params.id, { title, categories, description, thumbnail, content }) 65 | .then(() => { 66 | message.success("Đã cập nhật bài viết"); 67 | props.history.push(`/fpt/${props.match.params.id}`); 68 | }) 69 | .catch(() => { 70 | message.error("Có lỗi xảy ra"); 71 | }) 72 | .finally(() => setLoading(false)); 73 | } 74 | } 75 | 76 | return ( 77 | 78 | 79 | Soạn thảo bài viết 80 | 81 |
82 |
89 |

{isEdit ? "Cập nhật bài" : "Viết bài mới"}

90 | 91 | setTitle(e.target.value)} 94 | placeholder="Tiêu đề bài viết" 95 | style={{ marginBottom: "1rem" }} 96 | /> 97 | setCategories(e.target.value)} 100 | placeholder="Chuyên mục, phân cách bởi dấu phẩy" 101 | style={{ marginBottom: "1rem" }} 102 | /> 103 | {thumbnail && isImageURL(thumbnail) && ( 104 | Thumbnail 112 | )} 113 | setThumbnail(e.target.value)} 116 | placeholder="Link ảnh đại diện" 117 | style={{ marginBottom: "1rem" }} 118 | /> 119 |