├── server ├── .dockerignore ├── README.md ├── .gitignore ├── .husky │ └── pre-commit ├── .prettierrc.json ├── Dockerfile ├── src │ ├── api │ │ ├── user │ │ │ ├── index.js │ │ │ ├── user.controller.js │ │ │ └── user.service.js │ │ ├── compiler │ │ │ ├── index.js │ │ │ └── compiler.controller.js │ │ ├── index.js │ │ ├── room │ │ │ ├── index.js │ │ │ └── room.controller.js │ │ ├── auth │ │ │ ├── index.js │ │ │ ├── auth.controller.js │ │ │ └── auth.service.js │ │ └── question │ │ │ ├── index.js │ │ │ └── question.controller.js │ ├── common │ │ ├── errors │ │ │ └── AppError.js │ │ └── email │ │ │ └── email.js │ ├── middleware │ │ ├── verifyAdmin.js │ │ ├── verifyToken.js │ │ └── uploadFile.js │ └── models │ │ ├── roomModel.js │ │ ├── userModel.js │ │ ├── replyModel.js │ │ └── questionModel.js ├── package.json ├── app.js └── bin │ └── server.js ├── client ├── .dockerignore ├── .env ├── public │ ├── favicon.ico │ ├── image │ │ ├── bg.jpg │ │ ├── js.ico │ │ ├── vn.png │ │ ├── cpp.ico │ │ ├── css.png │ │ ├── demo.png │ │ ├── eng.ico │ │ ├── html.png │ │ ├── java.ico │ │ ├── java.png │ │ ├── logo.gif │ │ ├── csharp.ico │ │ ├── python.ico │ │ ├── tutorcatlogo.png │ │ ├── my-profile-pic.jpg │ │ └── remote-profile-pic.jpg │ └── vendor │ │ └── languageSwitcher.svg ├── postcss.config.js ├── @meowmeow │ ├── components │ │ ├── ProfileGroup │ │ │ ├── Menu │ │ │ │ ├── config.js │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── Badge │ │ │ └── index.js │ │ ├── Layout │ │ │ ├── Auth.js │ │ │ ├── TrunkeyAuth.js │ │ │ └── Forum.js │ │ ├── Tags │ │ │ ├── catatogies.js │ │ │ └── index.js │ │ ├── Header │ │ │ ├── Menu │ │ │ │ ├── config.js │ │ │ │ └── index.js │ │ │ ├── fullPage │ │ │ │ ├── trunkey.js │ │ │ │ └── index.js │ │ │ └── withSidebar │ │ │ │ └── index.js │ │ ├── Card │ │ │ └── index.js │ │ ├── PageComponents │ │ │ └── PageLoader.js │ │ ├── Loading │ │ │ └── index.js │ │ ├── TagsOverview │ │ │ └── index.js │ │ ├── QuestionDetail │ │ │ ├── Comment │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── Vote │ │ │ │ └── index.js │ │ │ ├── Detail │ │ │ │ └── index.js │ │ │ └── CommentBox │ │ │ │ └── index.js │ │ ├── QuestionOverview │ │ │ └── index.js │ │ ├── TagsDetail │ │ │ └── index.js │ │ ├── LanguageSwitcher │ │ │ └── index.js │ │ ├── Modal │ │ │ └── index.js │ │ ├── AnswerEdit │ │ │ └── index.js │ │ ├── QuestionNew │ │ │ └── backup.js │ │ └── auth │ │ │ ├── SignIn.js │ │ │ └── SignUp.js │ ├── utils │ │ ├── i18n │ │ │ ├── index.js │ │ │ ├── entries │ │ │ │ ├── en-US.js │ │ │ │ └── vi-VN.js │ │ │ └── dist │ │ │ │ └── index.dev.js │ │ ├── IntlMessages.js │ │ ├── GetTranslateText.js │ │ └── LangConfig.js │ ├── redux │ │ ├── actions │ │ │ ├── lang.js │ │ │ ├── user.js │ │ │ └── config.js │ │ ├── reducers │ │ │ ├── lang.js │ │ │ ├── user.js │ │ │ ├── index.js │ │ │ └── config.js │ │ └── configureStore.js │ ├── modules │ │ ├── apiService │ │ │ ├── config.js │ │ │ └── index.js │ │ └── index.js │ ├── authentication │ │ ├── auth-page-wrappers │ │ │ ├── AuthPage.js │ │ │ └── SecurePage.js │ │ ├── index.js │ │ └── auth-methods │ │ │ └── jwt-auth │ │ │ └── index.js │ └── styles │ │ ├── global.css │ │ └── style.css ├── config │ └── axios.js ├── pages │ ├── explorer.js │ ├── live │ │ └── [roomID].jsx │ ├── account │ │ ├── signin.js │ │ └── signup.js │ ├── tags │ │ ├── index.js │ │ └── [tid].js │ ├── ub-error.jsx │ ├── questions │ │ ├── new.js │ │ ├── index.js │ │ ├── edit │ │ │ └── [qid].js │ │ ├── [qid] │ │ │ └── answers │ │ │ │ └── edit │ │ │ │ └── [aid].js │ │ └── [qid].js │ ├── _error.js │ ├── _app.js │ └── index.js ├── .gitignore ├── Dockerfile ├── components │ ├── chat │ │ ├── RemoteChat.jsx │ │ ├── ChatBreak.jsx │ │ ├── MeChat.jsx │ │ ├── RemoteLeft.jsx │ │ ├── RemoteJoined.jsx │ │ ├── ChatBox.jsx │ │ └── ChatHeader.jsx │ ├── LoadingCover.jsx │ ├── Loading.jsx │ ├── ErrorPermissionMicroJoiner.jsx │ ├── ErrorPermissionCameraJoiner.jsx │ ├── ErrorPermissionMicroHost.jsx │ ├── ErrorUnsupportedBrowser.jsx │ ├── Error.jsx │ ├── HostLeft.jsx │ ├── OutputCodeFromMe.jsx │ └── OutputCodeFromRemote.jsx ├── tailwind.config.js ├── package.json └── README.md ├── yarn.lock ├── docker-compose.yml └── README.md /server/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | node_modules -------------------------------------------------------------------------------- /client/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SERVER_URL = http://localhost:5000 -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/server/README.md -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | #.husky 3 | .DS_Store 4 | .env 5 | config.env 6 | upload -------------------------------------------------------------------------------- /server/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/image/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/bg.jpg -------------------------------------------------------------------------------- /client/public/image/js.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/js.ico -------------------------------------------------------------------------------- /client/public/image/vn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/vn.png -------------------------------------------------------------------------------- /client/public/image/cpp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/cpp.ico -------------------------------------------------------------------------------- /client/public/image/css.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/css.png -------------------------------------------------------------------------------- /client/public/image/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/demo.png -------------------------------------------------------------------------------- /client/public/image/eng.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/eng.ico -------------------------------------------------------------------------------- /client/public/image/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/html.png -------------------------------------------------------------------------------- /client/public/image/java.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/java.ico -------------------------------------------------------------------------------- /client/public/image/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/java.png -------------------------------------------------------------------------------- /client/public/image/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/logo.gif -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | -------------------------------------------------------------------------------- /client/public/image/csharp.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/csharp.ico -------------------------------------------------------------------------------- /client/public/image/python.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/python.ico -------------------------------------------------------------------------------- /client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /client/public/image/tutorcatlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/tutorcatlogo.png -------------------------------------------------------------------------------- /client/public/image/my-profile-pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/my-profile-pic.jpg -------------------------------------------------------------------------------- /client/public/image/remote-profile-pic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trunkey2003/TutorCat/HEAD/client/public/image/remote-profile-pic.jpg -------------------------------------------------------------------------------- /client/@meowmeow/components/ProfileGroup/Menu/config.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from "../../../utils/IntlMessages"; 2 | 3 | const menu = [ 4 | ] 5 | 6 | export { 7 | menu 8 | }; -------------------------------------------------------------------------------- /client/config/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const Axios = axios.create({ 4 | baseURL: process.env.NEXT_PUBLIC_SERVER_URL, 5 | withCredentials: true, 6 | }) -------------------------------------------------------------------------------- /server/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } -------------------------------------------------------------------------------- /client/@meowmeow/utils/i18n/index.js: -------------------------------------------------------------------------------- 1 | import enLang from './entries/en-US'; 2 | import viLang from './entries/vi-VN'; 3 | 4 | const AppLocale = { 5 | vi: viLang, 6 | en: enLang, 7 | }; 8 | 9 | export default AppLocale; 10 | -------------------------------------------------------------------------------- /client/@meowmeow/redux/actions/lang.js: -------------------------------------------------------------------------------- 1 | export const setVi = () => { 2 | return { 3 | type: "SET_VI", 4 | }; 5 | }; 6 | 7 | export const setEng = () => { 8 | return { 9 | type: "SET_ENG", 10 | }; 11 | }; -------------------------------------------------------------------------------- /client/@meowmeow/utils/i18n/entries/en-US.js: -------------------------------------------------------------------------------- 1 | import enMessages from '../locales/en_US.json'; 2 | 3 | const EnLang = { 4 | messages: { 5 | ...enMessages, 6 | }, 7 | locale: 'en-US', 8 | }; 9 | export default EnLang; 10 | -------------------------------------------------------------------------------- /client/@meowmeow/utils/i18n/entries/vi-VN.js: -------------------------------------------------------------------------------- 1 | import viMessages from '../locales/vi_VN.json'; 2 | 3 | const ViLan = { 4 | messages: { 5 | ...viMessages, 6 | }, 7 | locale: 'vi-VN', 8 | }; 9 | 10 | export default ViLan; 11 | -------------------------------------------------------------------------------- /client/@meowmeow/redux/actions/user.js: -------------------------------------------------------------------------------- 1 | export const signIn = () => { 2 | return { 3 | type: "SIGN_IN", 4 | }; 5 | }; 6 | 7 | export const signOut = () => { 8 | return { 9 | type: "SIGN_OUT", 10 | }; 11 | }; -------------------------------------------------------------------------------- /client/pages/explorer.js: -------------------------------------------------------------------------------- 1 | 2 | import PageLoader from "../@meowmeow/components/PageComponents/PageLoader" 3 | 4 | const Explorer = () => { 5 | return ( 6 | 7 | ) 8 | } 9 | 10 | export default Explorer 11 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Badge/index.js: -------------------------------------------------------------------------------- 1 | 2 | const Badge = ({ text }) => { 3 | return ( 4 | {text} 5 | ); 6 | } 7 | 8 | export default Badge; 9 | 10 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | ### NextJS ### 2 | # Next build dir 3 | .next/ 4 | 5 | ### react ### 6 | .DS_* 7 | *.log 8 | logs 9 | **/*.backup.* 10 | **/*.back.* 11 | 12 | node_modules 13 | bower_components 14 | 15 | *.sublime* 16 | .env 17 | psd 18 | thumb 19 | sketch 20 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | ENV NODE_ENV=production 4 | 5 | WORKDIR /usr/src/app/server 6 | 7 | COPY ["package.json", "package-lock.json*", "./"] 8 | 9 | RUN npm install --production 10 | 11 | COPY . /usr/src/app/server 12 | 13 | CMD [ "npm", "start" ] -------------------------------------------------------------------------------- /client/@meowmeow/utils/IntlMessages.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage, injectIntl } from 'react-intl'; 3 | 4 | const InjectMassage = (props) => ; 5 | export default injectIntl(InjectMassage, { 6 | withRef: false, 7 | }); 8 | -------------------------------------------------------------------------------- /client/@meowmeow/utils/GetTranslateText.js: -------------------------------------------------------------------------------- 1 | import { useIntl } from 'react-intl'; 2 | 3 | const GetTranslateText = (textId) => { 4 | const intl18 = useIntl(); 5 | const plainText = intl18.formatMessage({ id: textId }); 6 | return plainText; 7 | }; 8 | 9 | export default GetTranslateText -------------------------------------------------------------------------------- /server/src/api/user/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const userController = require('./user.controller'); 3 | const { verifyToken } = require('../../middleware/verifyToken'); 4 | 5 | router.get('/get-Info', verifyToken, userController.getInfo); 6 | 7 | module.exports = router; 8 | -------------------------------------------------------------------------------- /client/@meowmeow/redux/reducers/lang.js: -------------------------------------------------------------------------------- 1 | export const Lang = (langCode = 1, action) => { 2 | switch (action.type) { 3 | case "SET_VI": 4 | return langCode = 2; 5 | case "SET_ENG": 6 | return langCode = 1; 7 | default: 8 | return langCode = 1; 9 | } 10 | }; 11 | export default Lang -------------------------------------------------------------------------------- /client/@meowmeow/redux/reducers/user.js: -------------------------------------------------------------------------------- 1 | export const User = (data = false, action) => { 2 | switch (action.type) { 3 | case "SIGN_IN": 4 | return data = true; 5 | case "SIGN_OUT": 6 | return data = false; 7 | default: 8 | return data = false; 9 | } 10 | }; 11 | export default User -------------------------------------------------------------------------------- /client/@meowmeow/redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | 3 | import Language from "./lang"; 4 | import User from "./user" 5 | 6 | import { Config } from "./config"; 7 | 8 | export default combineReducers({ 9 | // LangCode: Language, 10 | // User: User 11 | Config: Config, 12 | }); 13 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Layout/Auth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from '../Header/fullPage'; 3 | 4 | export default function Auth({ children }) { 5 | return ( 6 | <> 7 |
8 |
9 | {children} 10 |
11 |
12 | 13 | ); 14 | } -------------------------------------------------------------------------------- /client/@meowmeow/components/Layout/TrunkeyAuth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from '../Header/fullPage/trunkey'; 3 | 4 | export default function Auth({ children }) { 5 | return ( 6 | <> 7 |
8 |
9 | {children} 10 |
11 |
12 | 13 | ); 14 | } -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | RUN mkdir -p /usr/src/app/client 4 | ENV PORT 3000 5 | 6 | WORKDIR /usr/src/app/client 7 | 8 | COPY package.json /usr/src/app/client 9 | COPY yarn.lock /usr/src/app/client 10 | 11 | RUN yarn install 12 | 13 | COPY . /usr/src/app/client 14 | 15 | RUN yarn build 16 | 17 | EXPOSE 3000 18 | CMD [ "yarn", "start" ] -------------------------------------------------------------------------------- /server/src/api/user/user.controller.js: -------------------------------------------------------------------------------- 1 | const userService = require('./user.service'); 2 | module.exports = { 3 | getInfo: async (req, res, next) => { 4 | try { 5 | const DTO = await userService.getInfo(req.user); 6 | res.status(200).json(DTO); 7 | } catch (error) { 8 | next(error); 9 | } 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /server/src/common/errors/AppError.js: -------------------------------------------------------------------------------- 1 | class AppError extends Error { 2 | constructor(statusCode, msg) { 3 | super(); 4 | 5 | if (Error.captureStackTrace) { 6 | Error.captureStackTrace(this, AppError); 7 | } 8 | 9 | this.message = msg; 10 | this.statusCode = statusCode; 11 | } 12 | } 13 | 14 | module.exports = { AppError }; -------------------------------------------------------------------------------- /server/src/middleware/verifyAdmin.js: -------------------------------------------------------------------------------- 1 | const User = require("../models/userModel"); 2 | const {AppError} = require("../common/errors/AppError"); 3 | exports.verifyAdmin = (req,res,next) =>{ 4 | try { 5 | if(req.user.role !== 'admin') 6 | next(new AppError(401, "Unauthorized")); 7 | } catch(error) { 8 | throw new AppError(500, error.message); 9 | } 10 | } -------------------------------------------------------------------------------- /client/@meowmeow/components/Tags/catatogies.js: -------------------------------------------------------------------------------- 1 | 2 | const categories = ["javascript", "java", "python", "c#", "php", "android", "html", "jquery", "c++", "css", "ios", "r", "node.js", "reactjs", "arrays", "c", "ruby-on-rails", ".net", "sql-server", "python-3.x", "swift", "objective-c", "django", "angular", "angularjs", "excel", "regex", "pandas", "ruby", "iphone", "ajax", "linux"] 3 | 4 | export default categories; -------------------------------------------------------------------------------- /client/components/chat/RemoteChat.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function RemoteChat({ content }) { 4 | return ( 5 |
6 | 7 |

{content}

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /client/components/chat/ChatBreak.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function ChatBreak({content}) { 4 | return ( 5 |
6 |
7 | {content} 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/components/chat/MeChat.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function MeChat({content, children}) { 4 | return ( 5 |
6 |

{content || children}

7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /client/components/chat/RemoteLeft.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function RemoteJoin() { 4 | return ( 5 |
6 |
7 | Ho Quang Lam have left 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/components/chat/RemoteJoined.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function RemoteJoined() { 4 | return ( 5 |
6 |
7 | Ho Quang Lam have joined 8 |
9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/@meowmeow/redux/actions/config.js: -------------------------------------------------------------------------------- 1 | export const setVi = () => { 2 | return { 3 | type: "SET_VI", 4 | }; 5 | }; 6 | 7 | export const setEng = () => { 8 | return { 9 | type: "SET_ENG", 10 | }; 11 | }; 12 | 13 | export const signIn = () => { 14 | return { 15 | type: "SIGN_IN", 16 | }; 17 | }; 18 | 19 | export const signOut = () => { 20 | return { 21 | type: "SIGN_OUT", 22 | }; 23 | }; -------------------------------------------------------------------------------- /server/src/api/compiler/index.js: -------------------------------------------------------------------------------- 1 | var router = require('express').Router(); 2 | 3 | const compilerController = require('./compiler.controller'); 4 | 5 | router.post('/submission/create-and-get-result', compilerController.createSubmission, compilerController.getSubmission); 6 | router.get('/submission/get/:submissionId', compilerController.getSubmission); 7 | router.get('/submission/test', (req, res, next) => {res.send("hello");}) 8 | 9 | module.exports = router; -------------------------------------------------------------------------------- /client/@meowmeow/components/Header/Menu/config.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from "../../../utils/IntlMessages"; 2 | 3 | const PublicMenu = [ 4 | { "name": , "link": "/questions" }, 5 | { "name": , "link": "/tags" }, 6 | ] 7 | 8 | const NavMenu = [ 9 | { "name": , "link": "/" }, 10 | ]; 11 | 12 | export { 13 | NavMenu, 14 | PublicMenu 15 | }; -------------------------------------------------------------------------------- /client/@meowmeow/modules/apiService/config.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export const Axios = axios.create({ 4 | baseURL: `${process.env.NEXT_PUBLIC_SERVER_URL}/api/`, 5 | headers: { 6 | 'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE,PATCH', 7 | 'Access-Control-Allow-Credentials': true, 8 | 'Content-Type': 'application/json;charset=UTF-8', 9 | "Access-Control-Allow-Origin": "*", 10 | }, 11 | withCredentials: true, 12 | }); 13 | -------------------------------------------------------------------------------- /server/src/api/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const question = require('./question'); 3 | const auth = require('./auth/'); 4 | const user = require('./user/'); 5 | const room = require('./room/'); 6 | const compiler = require('./compiler'); 7 | 8 | router.use('/question', question); 9 | router.use('/auth', auth); 10 | router.use('/user', user); 11 | router.use('/room', room); 12 | router.use('/compiler', compiler); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Tags/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | const Tags = (props) => { 4 | const Tags = props.tags; 5 | return ( 6 |
7 | {Tags.map((row, index) => ( 8 | {row} 9 | ))} 10 |
11 | ); 12 | } 13 | 14 | export default Tags; 15 | 16 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Card/index.js: -------------------------------------------------------------------------------- 1 | const Card = ({ title, content, styleContent }) => { 2 | return ( 3 |
4 |
5 |

{title}

6 |

{content}

7 |
8 |
9 | ) 10 | } 11 | 12 | export default Card -------------------------------------------------------------------------------- /server/src/api/user/user.service.js: -------------------------------------------------------------------------------- 1 | const { AppError } = require('../../common/errors/AppError'); 2 | const User = require('../../models/userModel'); 3 | module.exports = { 4 | getInfo: async (user) => { 5 | try { 6 | return { 7 | statusCode: 200, 8 | message: 'Successfully get info', 9 | info: user, 10 | }; 11 | } catch (error) { 12 | throw new AppError(500, error.message); 13 | } 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /client/pages/live/[roomID].jsx: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | const LiveRoomCSR = dynamic(() => import("../../components/LiveRoom"), { ssr: false }); //Không tồn tại đối tượng navigator bên server 3 | import { useRouter } from "next/router"; 4 | import { useEffect } from "react"; 5 | 6 | export default function LiveRoom() { 7 | 8 | const route = useRouter(); 9 | useEffect(() => { 10 | }, [route]); 11 | 12 | return <>{route.query.roomID ? : ""}; 13 | } 14 | -------------------------------------------------------------------------------- /client/@meowmeow/components/PageComponents/PageLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import IntlMessages from '../../utils/IntlMessages' 3 | import { Hearts } from 'react-loader-spinner' 4 | 5 | const PageLoader = () => { 6 | return ( 7 |
8 |
9 | 10 |

11 |
12 |
13 | ); 14 | }; 15 | 16 | export default PageLoader; 17 | -------------------------------------------------------------------------------- /client/@meowmeow/utils/i18n/dist/index.dev.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _enUS = _interopRequireDefault(require("./entries/en-US")); 9 | 10 | var _viVN = _interopRequireDefault(require("./entries/vi-VN")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | var AppLocale = { 15 | vi: _viVN["default"], 16 | en: _enUS["default"] 17 | }; 18 | var _default = AppLocale; 19 | exports["default"] = _default; -------------------------------------------------------------------------------- /client/components/LoadingCover.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function LoadingCover() { 4 | return ( 5 |
6 |
7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /client/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | // import React from "react"; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |
7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /server/src/api/room/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const roomController = require('./room.controller'); 3 | 4 | router.get('/get/:roomID', roomController.getRoom); 5 | router.post('/add', roomController.addRoom); 6 | router.delete('/delete/:roomID', roomController.deleteRoom) 7 | router.delete('/delete-if-empty/:roomID', roomController.deleteRoomIfEmpty); 8 | router.put('/join/:roomID/:userID', roomController.increaseUserCount); 9 | router.put('/leave/:roomID/:userID', roomController.decreaseUserCount); 10 | router.get('/get', roomController.getAllAvailableRooms); 11 | 12 | module.exports = router; 13 | -------------------------------------------------------------------------------- /client/@meowmeow/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'redux' 2 | import { persistStore, persistReducer } from 'redux-persist' 3 | import storage from 'redux-persist/lib/storage' 4 | 5 | import rootReducer from "./reducers" 6 | import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; 7 | 8 | const persistConfig = { 9 | key: 'root', 10 | storage: storage, 11 | stateReconciler: autoMergeLevel2 // Xem thêm tại mục "Quá trình merge". 12 | }; 13 | 14 | const pReducer = persistReducer(persistConfig, rootReducer); 15 | 16 | export const store = createStore(pReducer); 17 | export const persistor = persistStore(store); -------------------------------------------------------------------------------- /client/@meowmeow/components/Layout/Forum.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Header from '../Header/withSidebar'; 3 | 4 | export default function Forum({ children }) { 5 | return ( 6 | <> 7 |
8 |
9 |
10 |
11 |
12 | {children} 13 |
14 |
15 |
16 |
17 |
18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /client/pages/account/signin.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import PageLoader from '../../@meowmeow/components/PageComponents/PageLoader'; 4 | import AuthPage from '../../@meowmeow/authentication/auth-page-wrappers/AuthPage'; 5 | import { Heading } from '../../@meowmeow/modules' 6 | 7 | const SignIn = dynamic(() => import('../../@meowmeow/components/auth/SignIn'), { 8 | loading: () => , 9 | }); 10 | 11 | const SignInPage = () => ( 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default SignInPage; -------------------------------------------------------------------------------- /client/pages/account/signup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import PageLoader from '../../@meowmeow/components/PageComponents/PageLoader'; 4 | import AuthPage from '../../@meowmeow/authentication/auth-page-wrappers/AuthPage'; 5 | import { Heading } from '../../@meowmeow/modules' 6 | 7 | const SignUp = dynamic(() => import('../../@meowmeow/components/auth/SignUp'), { 8 | loading: () => , 9 | }); 10 | 11 | const SignUpPage = () => ( 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default SignUpPage; -------------------------------------------------------------------------------- /client/@meowmeow/redux/reducers/config.js: -------------------------------------------------------------------------------- 1 | const initalState = { langCode: 1, loggedIn: false } 2 | 3 | export const Config = (data = initalState, action) => { 4 | switch (action.type) { 5 | case "SET_VI": 6 | return { 7 | ...data, 8 | langCode: 2, 9 | }; 10 | case "SET_ENG": 11 | return { 12 | ...data, 13 | langCode: 1, 14 | }; 15 | case "SIGN_IN": 16 | return { 17 | ...data, 18 | loggedIn: true, 19 | }; 20 | case "SIGN_OUT": 21 | return { 22 | ...data, 23 | loggedIn: false, 24 | }; 25 | default: 26 | return data; 27 | } 28 | }; 29 | 30 | export default Config -------------------------------------------------------------------------------- /client/@meowmeow/components/ProfileGroup/index.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from "../../utils/IntlMessages"; 2 | import { Menu } from "./Menu"; 3 | 4 | export default function ProfileGroup() { 5 | return ( 6 |
7 | 10 | 11 |
12 | ); 13 | } -------------------------------------------------------------------------------- /server/src/api/auth/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const authController = require('../auth/auth.controller'); 3 | const { verifyToken } = require('../../middleware/verifyToken'); 4 | const { verifyAdmin } = require('../../middleware/verifyAdmin'); 5 | 6 | router.post('/sign-in', authController.signIn); 7 | 8 | router.post('/sign-up', authController.signUp); 9 | 10 | router.delete('/sign-out', authController.signOut); 11 | 12 | router.post('/forget-password', authController.forgetPassword); 13 | 14 | router.post('/reset-password', authController.resetPassword); 15 | 16 | router.post('/update-password', verifyToken, authController.updatePassword); 17 | module.exports = router; 18 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Loading/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Component } from 'react'; 2 | 3 | import IntlMessages from '../../utils/IntlMessages' 4 | import Modal from '../Modal' 5 | 6 | const LoadingModal = ({loading}) => { 7 | const [open, setOpen] = useState(loading) 8 | return ( 9 | <> 10 | 14 | 15 |

16 |
17 | 18 | ) 19 | } 20 | 21 | export default LoadingModal; -------------------------------------------------------------------------------- /client/components/ErrorPermissionMicroJoiner.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | export default function ErrorPermissionMicroJoiner() { 5 | return ( 6 |
7 |
8 | 9 |
Please grant permission for microphone then try again
10 |
window.location.reload()}>Reload
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/pages/tags/index.js: -------------------------------------------------------------------------------- 1 | import Forum from '../../@meowmeow/components/Layout/Forum' 2 | import PageLoader from '../../@meowmeow/components/PageComponents/PageLoader' 3 | import dynamic from 'next/dynamic'; 4 | import { Heading } from '../../@meowmeow/modules' 5 | 6 | const TagsOverview = dynamic(() => import('../../@meowmeow/components/TagsOverview'), { 7 | loading: () => , 8 | }); 9 | 10 | const questionNewPage = () => { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default questionNewPage; -------------------------------------------------------------------------------- /client/components/ErrorPermissionCameraJoiner.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | export default function ErrorPermissionCameraJoiner() { 5 | return ( 6 |
7 |
8 | 9 |
Please grant permission for camera then try again
10 |
window.location.href = window.location.origin + '/live'}>Back to Home
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/components/ErrorPermissionMicroHost.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | export default function ErrorPermissionMicroHost() { 5 | return ( 6 |
7 |
8 | 9 |
Room crashed, please grant permission for microphone then try again
10 |
window.location.href = window.location.origin + '/live'}>Back to Home
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | tutorcat-client: 4 | image: trunkey/tutorcat-client 5 | stdin_open: true 6 | ports: 7 | - "3000:3000" 8 | networks: 9 | - mern-app 10 | tutorcat-server: 11 | image: trunkey/tutorcat-server 12 | environment: 13 | - NODE_ENV=docker 14 | - MONGO_URL=mongodb://mongo/example 15 | ports: 16 | - "5000:5000" 17 | networks: 18 | - mern-app 19 | depends_on: 20 | - mongo 21 | mongo: 22 | image: mongo:3.6.19-xenial 23 | ports: 24 | - "27017:27017" 25 | networks: 26 | - mern-app 27 | volumes: 28 | - mongo-data:/data/db 29 | networks: 30 | mern-app: 31 | driver: bridge 32 | volumes: 33 | mongo-data: 34 | driver: local -------------------------------------------------------------------------------- /client/@meowmeow/authentication/auth-page-wrappers/AuthPage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { useAuth } from '../index'; 4 | import PageLoader from '../../components/PageComponents/PageLoader'; 5 | 6 | // eslint-disable-next-line react/prop-types 7 | const AuthPage = ({ children }) => { 8 | const { loadingAuthUser, authUser, setError } = useAuth(); 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | if (!loadingAuthUser && authUser) { 13 | router.push('/').then((r) => r); 14 | } 15 | 16 | return () => setError(''); 17 | }, [authUser, loadingAuthUser]); 18 | 19 | return authUser && loadingAuthUser ? : children; 20 | }; 21 | 22 | export default AuthPage; 23 | -------------------------------------------------------------------------------- /client/@meowmeow/authentication/auth-page-wrappers/SecurePage.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import { useAuth } from '../index'; 4 | import PageLoader from '../../components/PageComponents/PageLoader'; 5 | 6 | // eslint-disable-next-line react/prop-types 7 | const SecurePage = ({ children }) => { 8 | const { loadingAuthUser, authUser, setError } = useAuth(); 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | if (loadingAuthUser && !authUser) { 13 | router.push('/account/signin').then((r) => r); 14 | } 15 | 16 | return () => setError(''); 17 | }, [authUser, loadingAuthUser]); 18 | 19 | return authUser && !loadingAuthUser ? children : ; 20 | }; 21 | 22 | export default SecurePage; 23 | -------------------------------------------------------------------------------- /client/@meowmeow/utils/LangConfig.js: -------------------------------------------------------------------------------- 1 | import { IntlProvider } from 'react-intl'; 2 | import AppLocale from './i18n'; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | 5 | export default function LangConfig({ children }) { 6 | const langCode = useSelector((res) => res.Config.langCode); 7 | // console.log("lang: "+langCode) 8 | switch (langCode) { 9 | case 1: 10 | var lang = AppLocale.en; 11 | break; 12 | case 2: 13 | var lang = AppLocale.vi; 14 | break; 15 | default: 16 | var lang = AppLocale.en; 17 | } 18 | return ( 19 | 23 | {children} 24 | 25 | ); 26 | } -------------------------------------------------------------------------------- /client/pages/ub-error.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function ErrorUnsupportedBrowser() { 4 | return ( 5 |
6 |
7 | 8 |
Unsupported browser error
9 |
We currently don't support this browser
10 |
window.location.href = window.location.origin + '/live'}>Back to Home
11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /client/components/ErrorUnsupportedBrowser.jsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | export default function ErrorUnsupportedBrowser() { 5 | return ( 6 |
7 |
8 | 9 |
Unsupported browser error
10 |
We currently don't support this browser
11 |
window.location.href = window.location.origin + '/live'}>Back to Home
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /client/components/chat/ChatBox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RemoteChat from "./chat/RemoteChat"; 3 | import MeChat from "./chat/MeChat"; 4 | import ChatHeader from "./chat/ChatHeader"; 5 | import RemoteJoined from "./chat/RemoteJoined"; 6 | import RemoteLeft from "./chat/RemoteLeft"; 7 | import ChatFooter from "./chat/ChatFooter"; 8 | 9 | export default function ChatBox() { 10 | return ( 11 |
12 | 13 |
17 | 18 |
19 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /server/src/models/roomModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const room = new Schema({ 5 | userJob: {type : String, required : true}, 6 | roomID: { type : String, require : true, unique : true}, 7 | title: {type : String, maxlength: 100}, 8 | language: {type : String}, 9 | programmingLanguages : {type: [String]}, 10 | userName1: { type: String, require : true, default : 'anonymous', maxlength: 30}, 11 | userName2: { type: String, maxlength: 30}, 12 | userID1: {type : String, require : true, default : ""}, 13 | userID2: {type : String, require : true, default : ""}, 14 | userCount: { 15 | type : Number, 16 | require : true, 17 | default : 0, 18 | min : 0, 19 | max : 2 20 | } 21 | }, 22 | { 23 | timestamps: true 24 | }); 25 | 26 | module.exports = mongoose.model('room', room); -------------------------------------------------------------------------------- /client/public/vendor/languageSwitcher.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/@meowmeow/authentication/index.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react'; 2 | import { useProvideAuth } from './auth-methods/jwt-auth'; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { signIn, signOut } from "../redux/actions/user"; 5 | 6 | const authContext = createContext(); 7 | // Provider component that wraps your app and makes auth object .. 8 | // ... available to any child component that calls useAuth(). 9 | 10 | export function AuthProvider({ children }) { 11 | const auth = useProvideAuth(); 12 | return {children}; 13 | } 14 | 15 | // Hook for child components to get the auth object ... 16 | // ... and re-render when it changes. 17 | 18 | export const useAuth = () => { 19 | return useContext(authContext); 20 | }; 21 | 22 | export const loggedIn = () =>{ 23 | const result = useSelector((res) => res.Config.loggedIn); 24 | return result; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /server/src/middleware/verifyToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { AppError } = require('../common/errors/AppError'); 3 | const User = require('../models/userModel'); 4 | exports.verifyToken = async (req, res, next) => { 5 | try { 6 | const token = req.cookies.token; 7 | if (!token) { 8 | next(new AppError(401, 'Token is not valid')); 9 | } 10 | const decodeToken = jwt.verify(token, process.env.JWT_SECRET_KEY); 11 | // console.log(decodeToken); 12 | let user = await User.findById(decodeToken.userId); 13 | if (!user) { 14 | next(new AppError(403, "This token doesn't belong to this user")); 15 | } 16 | if (user.changedPasswordAfter(decodeToken.iat)) { 17 | next(new AppError(403, 'Password changed')); 18 | } 19 | req.user = user; 20 | next(); 21 | } catch (error) { 22 | next(new AppError(500, error.message)); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /client/pages/questions/new.js: -------------------------------------------------------------------------------- 1 | import Forum from '../../@meowmeow/components/Layout/Forum' 2 | import PageLoader from '../../@meowmeow/components/PageComponents/PageLoader' 3 | import SecurePage from '../../@meowmeow/authentication/auth-page-wrappers/SecurePage' 4 | import { loggedIn } from '../../@meowmeow/authentication' 5 | import dynamic from 'next/dynamic'; 6 | import SignInPage from '../account/signin'; 7 | import { Heading } from '../../@meowmeow/modules' 8 | 9 | const QuestionNew = dynamic(() => import('../../@meowmeow/components/QuestionNew'), { 10 | loading: () => , 11 | }); 12 | 13 | const QuestionNewPage = () => { 14 | const authUser = loggedIn(); 15 | return ( 16 | <> 17 | {authUser ? <> 18 | 19 | < Forum > 20 | 21 | 22 | : } 23 | 24 | ) 25 | } 26 | 27 | export default QuestionNewPage; -------------------------------------------------------------------------------- /client/pages/questions/index.js: -------------------------------------------------------------------------------- 1 | import Forum from '../../@meowmeow/components/Layout/Forum' 2 | import QuestionOverview from '../../@meowmeow/components/QuestionOverview' 3 | import { Axios } from '../../@meowmeow/modules/apiService/config' 4 | import { Heading } from '../../@meowmeow/modules' 5 | 6 | const questionPage = ({ qAll }) => { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export async function getServerSideProps() { 18 | let data = await Axios 19 | .get(`/question/`) 20 | .then((res) => { 21 | let data = res.data.data 22 | return data 23 | }) 24 | .catch(() => { 25 | return null 26 | } 27 | ) 28 | return { 29 | props: { 30 | qAll: data || null, 31 | } 32 | } 33 | } 34 | 35 | export default questionPage; -------------------------------------------------------------------------------- /client/components/Error.jsx: -------------------------------------------------------------------------------- 1 | export default function Error() { 2 | return ( 3 |
17 |
18 |
19 |

404

20 | 21 |
22 | Oops! Page not found 23 |
24 | 25 |

26 | The room you attempt to join doesn’t exist. 27 |

28 | 29 | window.location = window.location.origin + '/live'} className="px-6 py-2 text-sm font-semibold text-blue-800 bg-blue-100 hover:cursor-pointer hover:bg-blue-200"> 30 | Go home 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/components/HostLeft.jsx: -------------------------------------------------------------------------------- 1 | export default function HostLeft() { 2 | return ( 3 |
17 |
18 |
19 |

404

20 | 21 |
22 | Oops! Meeting is over 23 |
24 | 25 |

26 | The host has left, the meeting is over ! 27 |

28 | 29 | window.location = window.location.origin + '/live'} className="px-6 py-2 text-sm font-semibold text-blue-800 bg-blue-100 hover:cursor-pointer hover:bg-blue-200"> 30 | Go home 31 | 32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | "./pages/**/*.{js,ts,jsx,tsx}", 4 | "./components/**/*.{js,ts,jsx,tsx}", 5 | "./@meowmeow/components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | }, 9 | plugins: [require("daisyui")], 10 | daisyui: { 11 | themes: [ 12 | { 13 | 'mytheme': { 14 | 'primary': '#7dd3fc', 15 | 'primary-focus': '#96e0ff', 16 | 'primary-content': '#ffffff', 17 | 'secondary': '#f000b8', 18 | 'secondary-focus': '#bd0091', 19 | 'secondary-content': '#ffffff', 20 | 'accent': '#37cdbe', 21 | 'accent-focus': '#2aa79b', 22 | 'accent-content': '#ffffff', 23 | 'neutral': '#3d4451', 24 | 'neutral-focus': '#2a2e37', 25 | 'neutral-content': '#ffffff', 26 | 'base-100': '#ffffff', 27 | 'base-200': '#f9fafb', 28 | 'base-300': '#d1d5db', 29 | 'base-content': '#1f2937', 30 | 'info': '#2094f3', 31 | 'success': '#009485', 32 | 'warning': '#ff9900', 33 | 'error': '#ff5724', 34 | }, 35 | }, 36 | ], 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /client/components/chat/ChatHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function RemoteHeader() { 4 | return ( 5 |
6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |
17 | Ho Quang Lam 18 |
19 | Student 20 |
21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /server/src/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const validator = require('validator'); 3 | const Schema = mongoose.Schema; 4 | 5 | const userSchema = new Schema({ 6 | name: { 7 | type: String, 8 | required: [true, 'Please tell us your name!'], 9 | }, 10 | email: { 11 | type: String, 12 | required: [true, 'Please provide your email'], 13 | unique: true, 14 | lowercase: true, 15 | validate: [validator.isEmail, 'Please provide a valid email'], 16 | }, 17 | role: { 18 | type: String, 19 | enum: ['user', 'admin'], 20 | default: 'user', 21 | }, 22 | password: { 23 | type: String, 24 | required: true, 25 | minlength: 8, 26 | select: false, 27 | }, 28 | passwordChangedAt: Date, 29 | passwordResetToken: String, 30 | passwordResetExpires: Date, 31 | }); 32 | 33 | userSchema.methods.changedPasswordAfter = (JWTTimestamp) => { 34 | if (this.passwordChangedAt) { 35 | const changedTimestamp = parseInt(this.passwordChangedAt.getTime() / 1000, 10); 36 | return JWTTimestamp < changedTimestamp; 37 | } 38 | return false; 39 | }; 40 | 41 | module.exports = mongoose.model('User', userSchema); 42 | -------------------------------------------------------------------------------- /client/@meowmeow/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .show-chat-box { 6 | animation: scroll-out-chat-box 0.70022003s ease; 7 | width: 700px; 8 | } 9 | 10 | .hide-chat-box { 11 | animation: scroll-in-chat-box 0.70022003s ease; 12 | width: 0; 13 | } 14 | 15 | @keyframes scroll-out-chat-box { 16 | from { 17 | width: 0; 18 | opacity: 0; 19 | } 20 | 21 | to { 22 | width: 700px; 23 | opacity: 1; 24 | } 25 | } 26 | 27 | @keyframes scroll-in-chat-box { 28 | from { 29 | width: 700px; 30 | opacity: 1; 31 | } 32 | 33 | to { 34 | width: 0; 35 | opacity: 0; 36 | } 37 | } 38 | 39 | .move-out-chat-toogle-button { 40 | animation: move-out-chat-toogle-button 0.70022003s ease; 41 | left: 700px; 42 | } 43 | 44 | .move-in-chat-toogle-button { 45 | animation: move-in-chat-toogle-button 0.70022003s ease; 46 | left: 0; 47 | } 48 | 49 | @keyframes move-out-chat-toogle-button { 50 | from { 51 | left: 0; 52 | } 53 | 54 | to { 55 | left: 700px; 56 | } 57 | } 58 | 59 | @keyframes move-in-chat-toogle-button { 60 | from { 61 | left: 700px; 62 | } 63 | 64 | to { 65 | left: 0; 66 | } 67 | } -------------------------------------------------------------------------------- /client/@meowmeow/components/TagsOverview/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react' 2 | import Card from '../Card' 3 | import IntlMessages from '../../utils/IntlMessages' 4 | import categories from '../Tags/catatogies' 5 | 6 | const TagsOverview = () => { 7 | return ( 8 | <> 9 |
10 | {categories.map((tag) => ( 11 | <> 12 | 13 |
14 | } 16 | content={} 17 | styleContent="text-sm" /> 18 |
19 |
20 | 21 | ) 22 | )} 23 |
24 |
25 |

26 |
27 | 28 | 29 | ) 30 | } 31 | 32 | export default TagsOverview; -------------------------------------------------------------------------------- /server/src/models/replyModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const replySchema = new Schema({ 6 | userID: { 7 | type: Schema.Types.ObjectId, 8 | ref: 'User', 9 | required: true, 10 | }, 11 | questionID: { 12 | type: Schema.Types.ObjectId, 13 | ref: 'Question', 14 | required: true, 15 | }, 16 | dateCreated: { 17 | type: Date, 18 | default: Date.now, 19 | }, 20 | content: { 21 | type: String, 22 | required: true, 23 | }, 24 | numUpVote: { 25 | type: Number, 26 | default: 0, 27 | }, 28 | userUpVote: [ 29 | { 30 | _id: false, 31 | userID: { 32 | type: Schema.Types.ObjectId, 33 | required: true, 34 | }, 35 | }, 36 | ], 37 | numDownVote: { 38 | type: Number, 39 | default: 0, 40 | }, 41 | userDownVote: [ 42 | { 43 | _id: false, 44 | userID: { 45 | type: Schema.Types.ObjectId, 46 | required: true, 47 | }, 48 | }, 49 | ], 50 | isChanged: { 51 | type: Boolean, 52 | required: true, 53 | default: false, 54 | }, 55 | }); 56 | 57 | module.exports = mongoose.model('Reply', replySchema); 58 | -------------------------------------------------------------------------------- /client/@meowmeow/modules/apiService/index.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Axios } from './config' 3 | import toast from 'react-hot-toast' 4 | 5 | const newPost = async (question, callbackFun) => { 6 | await Axios 7 | .post('/question/add/', question) 8 | .then(({ data }) => { 9 | if (data.statusCode == "200") { 10 | return data.data; 11 | } 12 | else { 13 | return null; 14 | } 15 | }) 16 | .catch(function (error) { 17 | return null; 18 | }) 19 | } 20 | 21 | 22 | const getQuesByQuesId = async (questionId) => { 23 | await Axios 24 | .get(`/question/${questionId}/detail`) 25 | .then(res => { 26 | let data = res.data.data 27 | return data 28 | }) 29 | .catch(() => { 30 | return null 31 | } 32 | ) 33 | } 34 | 35 | const getAllQues = async () => { 36 | await Axios 37 | .get(`/question/`) 38 | .then(res => { 39 | let data = res.data.data 40 | return data 41 | }) 42 | .catch(() => { 43 | return null 44 | } 45 | ) 46 | } 47 | 48 | const upVote = async () => { 49 | await Axios 50 | .get(`/question/`) 51 | .then(res => { 52 | let data = res.data.data 53 | return data 54 | }) 55 | .catch(() => { 56 | return null 57 | } 58 | ) 59 | } 60 | 61 | export { 62 | newPost, 63 | getQuesByQuesId, 64 | getAllQues 65 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "@emotion/react": "^11.9.0", 10 | "@emotion/styled": "^11.8.1", 11 | "@monaco-editor/react": "^4.4.4", 12 | "@mui/icons-material": "^5.6.2", 13 | "@mui/material": "^5.6.4", 14 | "next": "latest", 15 | "peerjs": "^1.3.2", 16 | "react": "17.0.2", 17 | "react-dom": "^16.0.0", 18 | "react-draggable": "^4.4.5", 19 | "socket.io-client": "^4.5.0" 20 | }, 21 | "devDependencies": { 22 | "@reduxjs/toolkit": "^1.8.1", 23 | "@writergate/quill-image-uploader-nextjs": "^0.1.8", 24 | "autoprefixer": "^10.4.5", 25 | "axios": "^0.26.1", 26 | "daisyui": "^2.14.2", 27 | "dayjs": "^1.11.1", 28 | "highlight.js": "^11.5.1", 29 | "html-react-parser": "^1.4.12", 30 | "postcss": "^8.4.12", 31 | "prop-types": "^15.8.1", 32 | "react-hot-toast": "^2.2.0", 33 | "react-intl": "^5.25.0", 34 | "react-loader-spinner": "^6.0.0-0", 35 | "react-quill": "1.3.5", 36 | "react-redux": "^8.0.1", 37 | "react-select": "^5.3.1", 38 | "reactjs-popup": "^2.0.5", 39 | "redux": "^4.2.0", 40 | "redux-persist": "^6.0.0", 41 | "tailwindcss": "^3.0.24", 42 | "react-copy-to-clipboard": "^5.1.0", 43 | "html-to-text": "^8.2.0", 44 | "detect-inapp": "^1.4.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /server/src/common/email/email.js: -------------------------------------------------------------------------------- 1 | const nodemailer = require('nodemailer'); 2 | const User = require("../../models/userModel"); 3 | const {AppError} = require('../../common/errors/AppError'); 4 | async function sendEmail(Email, resetToken) { 5 | try{ 6 | let transporter = nodemailer.createTransport({ 7 | host: process.env.EMAIL_HOST, 8 | port: process.env.EMAIL_PORT, 9 | secure: true, 10 | auth: { 11 | type: 'OAUTH2', 12 | user: process.env.EMAIL_USERNAME, 13 | clientId: process.env.CLIENT_ID, 14 | clientSecret: process.env.CLIENT_SECRET, 15 | accessToken: process.env.ACCESS_TOKEN, 16 | refreshToken: process.env.REFRESH_TOKEN, 17 | }, 18 | }); 19 | const mailOptions = { 20 | from: ``, 21 | to: Email, 22 | subject: "Reset password", 23 | text: `Click here to reset your password ..../${resetToken}`, 24 | }; 25 | await transporter.sendMail(mailOptions); 26 | let Token = await User.findOne({email: Email}); 27 | Token.passwordResetToken = resetToken; 28 | return { 29 | statusCode: 200, 30 | message: "Send successfully", 31 | } 32 | }catch(error){ 33 | throw new AppError(500, error.message); 34 | } 35 | } 36 | 37 | module.exports = sendEmail; 38 | -------------------------------------------------------------------------------- /client/pages/_error.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from "../@meowmeow/utils/IntlMessages"; 2 | 3 | function Error({ statusCode }) { 4 | return ( 5 | 33 | ); 34 | } 35 | 36 | Error.getInitialProps = ({ res, err }) => { 37 | const statusCode = res ? res.statusCode : err ? err.statusCode : 404 38 | return { statusCode } 39 | } 40 | 41 | export default Error -------------------------------------------------------------------------------- /server/src/api/question/index.js: -------------------------------------------------------------------------------- 1 | const router = require('express').Router(); 2 | const questionController = require('./question.controller'); 3 | const upload = require('../../middleware/uploadFile'); 4 | const { verifyToken } = require('../../middleware/verifyToken'); 5 | 6 | router.post('/upload', upload, questionController.uploadImage); 7 | router.get('/', questionController.getAllQuestion); 8 | router.get('/:id/detail', questionController.getQuestionWithID); 9 | router.get('/user/', verifyToken, questionController.getQuestionWithUserID); 10 | router.post('/add', verifyToken, questionController.addQuestion); 11 | router.delete('/:id/delete', verifyToken, questionController.deleteQuestion); 12 | router.patch('/:id/modify', verifyToken, questionController.modifyQuestion); 13 | router.post('/:id/up-vote', verifyToken, questionController.upVoteQuestion); 14 | router.post('/:id/down-vote', verifyToken, questionController.downVoteQuestion); 15 | router.get('/:id/reply', questionController.getAllReply); 16 | router.post('/:id/reply/add', verifyToken, questionController.addReply); 17 | router.delete('/reply/:id/delete', verifyToken, questionController.deleteReply); 18 | router.patch('/reply/:id/modify', verifyToken, questionController.modifyReply); 19 | router.post('/reply/:id/up-vote', verifyToken, questionController.upVoteReply); 20 | router.post('/reply/:id/down-vote', verifyToken, questionController.downVoteReply); 21 | router.get('/catalogue', questionController.getCatalogue); 22 | router.get('/catalogue/:category', questionController.getQuestionWithCategory); 23 | module.exports = router; 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutor Cat 2 | #### _Hệ thống diễn đàn và trao đổi về lập trình_ 3 | Dự án tham gia cuộc thi WebDev Adventure 2022 - UIT do Meowmeow team thực hiện 4 | 5 | ## Demo 6 | [Tutor Cat](https://tutorcat.vercel.app/) 7 | 8 | ## Tính năng 9 | - Thảo luận, trao đổi qua các câu hỏi và các chủ đề được tạo trên diễn đàn 10 | - Tạo meeting video Q&A không cần tài khoản nhanh chóng và tiện lợi trong chưa đến 1 phút. Gửi code và chạy code trên web với thời gian thực 11 | - ... [Đang phát triển] 12 | 13 | ## Công nghệ 14 | Team sử dụng NodeJS, NextJS, PeerJS & SocketIO và một số thư viện khác 15 | 16 | ## Cài đặt 17 | ### Sử dụng Docker 18 | Ở thư mục root chứa 2 folder client & server, mở terminal, chạy lệnh ``docker-compose up`` 19 | ### Đối với Front-end 20 | Hệ thống yêu cầu máy chủ cần có [Node.js](https://nodejs.org/) v16+ để khởi tạo. 21 | Các bước tiến hành cài đặt và khởi chạy được hướng dẫn với máy đã cài đặt sẵn gói npm hoặc yarn 22 | #### Trước khi sử dụng phần Frontend 23 | Cần cài đặt tất cả các gói trong package.json 24 | - Qua npm: ``npm install`` 25 | - Qua yarn: ``yarn`` 26 | #### Môi trường lập trình 27 | - Qua npm: ``npm run dev`` 28 | - QUA yarn: ``yarn dev`` 29 | #### Sản phẩm 30 | Đầu tiên, chúng ta cần xây dựng hệ thống: 31 | - Qua npm: ``npm run build`` 32 | - Qua yarn: ``yarn build`` 33 | 34 | Tiếp theo, chạy lệnh dưới đây để khởi chạy hệ thống: 35 | - Qua npm: ``npm start`` 36 | - Qua yarn: ``yarn start`` 37 | 38 | ### Đối với Back-end 39 | Cần cài đặt tất cả các gói trong package.json 40 | - Qua npm: ``npm install`` 41 | - Qua yarn: ``yarn`` 42 | #### Môi trường lập trình 43 | - Qua npm: ``npm run dev`` 44 | - QUA yarn: ``yarn dev`` 45 | #### Môi trường sản phẩm 46 | - Qua npm: ``npm start`` 47 | - Qua yarn: ``yarn start`` 48 | 49 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hackathon2022", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "lint-staged": { 7 | "*.{js}": "prettier --config .prettierrc.json server.js \"src/**/*.js\" --write" 8 | }, 9 | "scripts": { 10 | "lint": "lint-staged --allow-empty", 11 | "husky-prepare": "husky install", 12 | "dev": "nodemon ./bin/server", 13 | "start": "node ./bin/server", 14 | "pret": "prettier --config .prettierrc.json server.js \"src/**/*.js\" --write" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/tr1ggerbug/hackathon2022.git" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/tr1ggerbug/hackathon2022/issues" 24 | }, 25 | "homepage": "https://github.com/tr1ggerbug/hackathon2022#readme", 26 | "dependencies": { 27 | "axios": "^0.27.2", 28 | "bcrypt": "^5.0.1", 29 | "bcryptjs": "^2.4.3", 30 | "cloudinary": "^1.28.1", 31 | "cookie-parser": "^1.4.6", 32 | "cors": "^2.8.5", 33 | "crypto": "^1.0.1", 34 | "dotenv": "^14.2.0", 35 | "express": "^4.17.2", 36 | "express-mongo-sanitize": "^2.2.0", 37 | "http": "^0.0.1-security", 38 | "husky": "^7.0.4", 39 | "jsonwebtoken": "^8.5.1", 40 | "mongoose": "^6.1.7", 41 | "multer": "^1.4.4", 42 | "multer-storage-cloudinary": "^4.0.0", 43 | "nodemailer": "^6.7.2", 44 | "nodemon": "^2.0.15", 45 | "path": "^0.12.7", 46 | "request": "^2.88.2", 47 | "socket.io": "^4.5.0", 48 | "ts-node": "^10.7.0", 49 | "typescript": "^4.6.3", 50 | "uuidv4": "^6.2.13", 51 | "validator": "^13.7.0" 52 | }, 53 | "devDependencies": { 54 | "lint-staged": "^12.2.0", 55 | "prettier": "^2.5.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionDetail/Comment/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react' 2 | import toast from 'react-hot-toast' 3 | import CommentBox from '../CommentBox' 4 | import Detail from '../Detail' 5 | import IntlMessages from '../../../utils/IntlMessages' 6 | import { loggedIn } from '../../../authentication/index' 7 | import { Axios } from '../../../modules/apiService/config' 8 | 9 | const Comment = ({ qDetail, qAnswer }) => { 10 | const authUser = loggedIn(); 11 | const [comments, setComments] = useState(qAnswer) 12 | const update = (qDetail) => { 13 | Axios 14 | .get(`/question/${qDetail._id}/reply`) 15 | .then(({ data }) => { 16 | setComments(data.data) 17 | toast.success() 18 | }) 19 | .catch(() => { 20 | console.log(null) 21 | } 22 | ) 23 | } 24 | return ( 25 | <> 26 |
27 |

{comments.length} {(comments.length > 1) ? : }

28 | {comments.map((comment) => ( 29 | <> 30 | 31 |
32 | 33 | ) 34 | )} 35 |
36 | {authUser ? update(qDetail)} /> : <>} 37 | 38 | ) 39 | } 40 | 41 | export default Comment 42 | -------------------------------------------------------------------------------- /server/src/middleware/uploadFile.js: -------------------------------------------------------------------------------- 1 | const promisify = require('util').promisify; 2 | const multer = require('multer'); 3 | const path = require('path'); 4 | const cloudinary = require('cloudinary').v2; 5 | const { CloudinaryStorage } = require('multer-storage-cloudinary'); 6 | // const storage = multer.diskStorage({ 7 | // destination: (req, file, cb) => { 8 | // cb(null, './upload/'); 9 | // }, 10 | // filename: (req, file, cb) => { 11 | // const filename = 12 | // Date.now() + 13 | // '-' + 14 | // Math.round(Math.random() + 1e9) + 15 | // '-' + 16 | // file.originalname.toLowerCase().split(' ').join('_'); 17 | // cb(null, filename); 18 | // }, 19 | // }); 20 | 21 | const storage = new CloudinaryStorage({ 22 | cloudinary: cloudinary, 23 | params: async (req, file) => { 24 | return { 25 | folder: 'upload', 26 | format: 'jpg', 27 | public_id: 28 | Date.now() + 29 | '-' + 30 | Math.round(Math.random() + 1e9) + 31 | '-' + 32 | file.originalname 33 | .toLowerCase() 34 | .split(' ') 35 | .join('_') 36 | .replace(/\.jpeg|\.jpg|\.png/gi, ''), 37 | }; 38 | }, 39 | }); 40 | 41 | const fileFilter = (req, file, cb) => { 42 | if (file.mimetype == 'image/png' || file.mimetype == 'image/jpg' || file.mimetype == 'image/jpeg') { 43 | cb(null, true); 44 | } else { 45 | cb(null, false); 46 | const err = new Error('Only .png, .jpg and .jpeg format allowed!'); 47 | err.name = 'ExtensionError'; 48 | return cb(err); 49 | } 50 | }; 51 | 52 | var uploadFiles = multer({ 53 | storage, 54 | limits: { fileSize: 1 * 1024 * 1024 }, // 1MB 55 | fileFilter: fileFilter, 56 | }).single('photo'); 57 | var uploadFilesMiddleware = promisify(uploadFiles); 58 | module.exports = uploadFilesMiddleware; 59 | -------------------------------------------------------------------------------- /client/@meowmeow/components/ProfileGroup/Menu/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import Link from "next/link"; 3 | import { useRouter } from 'next/router'; 4 | import { menu } from "./config"; 5 | import IntlMessages from '../../../utils/IntlMessages'; 6 | import { getLocalStorage, setLocalStorage } from "../../../modules" 7 | import { useAuth } from '../../../authentication/index' 8 | 9 | const Menu = (props) => { 10 | const { userSignOut } = useAuth(); 11 | let user = JSON.parse(getLocalStorage("user", true, null)) 12 | // console.log(user) 13 | const router = useRouter(); 14 | let name = (user === undefined || user === null) ? "" : user.name 15 | return ( 16 | 45 | ); 46 | } 47 | 48 | export { 49 | Menu 50 | }; -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionOverview/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react' 2 | import IntlMessages from '../../utils/IntlMessages' 3 | import toast from 'react-hot-toast' 4 | import Link from 'next/link' 5 | 6 | const QuestionOverview = ({ data }) => { 7 | return ( 8 | <> 9 |
10 |
11 |

12 |
13 | 14 | 15 | 16 |
17 |
18 |

{data.length} {(data.length > 1) ? : }

19 | 34 | 35 |
36 | 37 | ) 38 | } 39 | 40 | export default QuestionOverview; -------------------------------------------------------------------------------- /server/src/models/questionModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const Schema = mongoose.Schema; 4 | 5 | const questionSchema = new Schema({ 6 | userID: { 7 | type: Schema.Types.ObjectId, 8 | ref: 'User', 9 | required: true, 10 | }, 11 | categories: { 12 | type: [ 13 | { 14 | _id: false, 15 | category: { 16 | type: String, 17 | required: true, 18 | }, 19 | }, 20 | ], 21 | required: true, 22 | }, 23 | title: { 24 | type: String, 25 | required: true, 26 | }, 27 | content: { 28 | type: String, 29 | required: true, 30 | }, 31 | // images: [ 32 | // { 33 | // _id: false, 34 | // imageUrl: { 35 | // type: String, 36 | // required: true, 37 | // }, 38 | // }, 39 | // ], 40 | // anonymous: { 41 | // type: Boolean, 42 | // required: true, 43 | // default: false, 44 | // }, 45 | dateCreated: { 46 | type: Date, 47 | default: Date.now, 48 | }, 49 | numUpVote: { 50 | type: Number, 51 | default: 0, 52 | }, 53 | userUpVote: { 54 | type: [ 55 | { 56 | _id: false, 57 | userID: { 58 | type: Schema.Types.ObjectId, 59 | required: true, 60 | }, 61 | }, 62 | ], 63 | }, 64 | numDownVote: { 65 | type: Number, 66 | default: 0, 67 | }, 68 | userDownVote: [ 69 | { 70 | _id: false, 71 | userID: { 72 | type: Schema.Types.ObjectId, 73 | required: true, 74 | }, 75 | }, 76 | ], 77 | isChanged: { 78 | type: Boolean, 79 | required: true, 80 | default: false, 81 | }, 82 | }); 83 | 84 | module.exports = mongoose.model('Question', questionSchema); 85 | -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionDetail/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, Component } from 'react' 2 | import IntlMessages from '../../utils/IntlMessages' 3 | import 'react-quill/dist/quill.snow.css' 4 | import 'highlight.js/styles/base16/solarized-light.css' 5 | import Detail from './Detail' 6 | import { loggedIn } from '../../authentication/index' 7 | import Tags from '../Tags' 8 | import Comment from './Comment' 9 | 10 | function QuestionDetail(props) { 11 | // const [qDetail, setQDetail] = useState() 12 | // const questionId = getQid() 13 | // useEffect(() => { 14 | // Axios 15 | // .get(`/question/${questionId}/detail`) 16 | // .then(({ data }) => { 17 | // setQDetail(data.data) 18 | // }) 19 | // .catch(() => { 20 | // console.log(null) 21 | // } 22 | // ) 23 | // }) 24 | 25 | const qDetail = props.data 26 | const qAnswer = props.answer 27 | const authUser = loggedIn(); 28 | return ( 29 | <> 30 |
31 |
32 |
33 |

{qDetail.title}

34 | [category.category])} /> 35 |
36 |
37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 | 45 | // : <> 46 | // 47 | // 48 | ) 49 | } 50 | 51 | export default QuestionDetail; 52 | 53 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const mongoSanitize = require('express-mongo-sanitize'); 2 | const express = require('express'); 3 | const path = require('path'); 4 | const mongoose = require('mongoose'); 5 | const cookieParser = require('cookie-parser'); 6 | const app = express(); 7 | const cors = require('cors'); 8 | const dotenv = require('dotenv'); 9 | const api = require('./src/api'); 10 | const cloudinary = require('cloudinary').v2; 11 | dotenv.config(); 12 | 13 | cloudinary.config({ 14 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 15 | api_key: process.env.CLOUDINARY_API_KEY, 16 | api_secret: process.env.CLOUDINARY_API_SECRET, 17 | }); 18 | 19 | app.use(cookieParser()); 20 | app.use(express.json()); 21 | 22 | const corsConfig = { 23 | credentials: true, 24 | origin: [process.env.clientI, process.env.clientII], 25 | }; 26 | 27 | app.use(cors(corsConfig)); 28 | app.use(express.urlencoded({ extended: true })); 29 | 30 | // Data sanitization against NoSQL query injection 31 | app.use(mongoSanitize()); 32 | 33 | if (process.env.NODE_ENV === 'docker'){ 34 | mongoose.connect(process.env.MONGO_URL, { 35 | useNewUrlParser: true, 36 | useUnifiedTopology: true, 37 | }) 38 | .then(() => console.log('Docker DB connection successful!')) 39 | .catch((err) => console.log(err)); 40 | } else { 41 | const DB = process.env.DATABASE.replace('', process.env.DATABASE_USERNAME) 42 | .replace('', process.env.DATABASE_PASSWORD) 43 | .replace('', process.env.DATABASE_NAME); 44 | mongoose 45 | .connect(DB, { 46 | useUnifiedTopology: true, 47 | useNewUrlParser: true, 48 | }) 49 | .then(() => console.log('DB connection successful!')) 50 | .catch((err) => console.log(err)); 51 | } 52 | 53 | app.use('/api', api); 54 | 55 | app.use((req, res) => { 56 | res.status(404).sendFile(path.join(__dirname, '/public/404.html')); 57 | }); 58 | 59 | app.use((error, req, res, next) => { 60 | let { statusCode, message } = error; 61 | 62 | statusCode = statusCode ? statusCode : 500; 63 | 64 | res.status(statusCode).json({ 65 | statusCode, 66 | message, 67 | }); 68 | }); 69 | 70 | module.exports = app; 71 | -------------------------------------------------------------------------------- /client/@meowmeow/components/TagsDetail/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useEffect } from 'react' 2 | import IntlMessages from '../../utils/IntlMessages' 3 | import toast from 'react-hot-toast' 4 | import Link from 'next/link' 5 | 6 | const TagsDetail = ({ data, name }) => { 7 | // console.log(data) 8 | return ( 9 | <> 10 |
11 |
12 |

13 |
14 | 15 | 16 | 17 |
18 |
19 |

20 | {name} 21 |

22 |

23 | 24 |

25 |

{data.length} {(data.length > 1) ? : }

26 | 41 | 42 |
43 | 44 | ) 45 | } 46 | 47 | export default TagsDetail; -------------------------------------------------------------------------------- /server/src/api/auth/auth.controller.js: -------------------------------------------------------------------------------- 1 | const authService = require('../auth/auth.service'); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | module.exports = { 5 | signUp: async (req, res, next) => { 6 | try { 7 | const DTO = await authService.signUp(req.body); 8 | res.status(200).json(DTO); 9 | } catch (error) { 10 | next(error); 11 | } 12 | }, 13 | signIn: async (req, res, next) => { 14 | try { 15 | const DTO = await authService.signIn(req.body); 16 | res.cookie('token', DTO.token, { 17 | sameSite: 'none', 18 | secure: true, 19 | httpOnly: true, 20 | maxAge: 3600000 * 24, 21 | }); 22 | delete DTO.token; 23 | res.status(200).json(DTO); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }, 28 | signOut: async (req, res, next) => { 29 | try { 30 | res.cookie('token', 'clear', { 31 | sameSite: 'none', 32 | secure: true, 33 | httpOnly: true, 34 | maxAge: 0, 35 | }); 36 | res.status(200).json({ 37 | statusCode: 200, 38 | message: 'Signed out successfully', 39 | }); 40 | } catch (error) { 41 | next(error); 42 | } 43 | }, 44 | forgetPassword: async (req, res, next) => { 45 | try { 46 | const DTO = await authService.forgetPassword(req.body); 47 | res.status(200).json(DTO); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }, 52 | resetPassword: async (req, res, next) => { 53 | try { 54 | const DTO = await authService.resetPassword(req.body); 55 | res.status(200).json(DTO); 56 | } catch (error) { 57 | next(error); 58 | } 59 | }, 60 | updatePassword: async (req, res, next) => { 61 | try { 62 | const DTO = await authService.updatePassword(req.user.id, req.body); 63 | res.cookie('token', DTO.token); 64 | delete DTO.token; 65 | res.status(200).json(DTO); 66 | } catch (error) { 67 | next(error); 68 | } 69 | }, 70 | }; 71 | -------------------------------------------------------------------------------- /client/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Provider } from "react-redux"; 3 | import { PersistGate } from 'redux-persist/integration/react' 4 | import Head from 'next/head'; 5 | import '../@meowmeow/styles/global.css'; 6 | import "../@meowmeow/styles/style.css"; 7 | import { AuthProvider } from '../@meowmeow/authentication'; 8 | import LangConfig from '../@meowmeow/utils/LangConfig'; 9 | import { persistor, store } from "../@meowmeow/redux/configureStore"; 10 | import toast, { Toaster } from 'react-hot-toast'; 11 | 12 | function MyApp({ Component, pageProps }) { 13 | return ( 14 | 15 | 16 | 17 | 18 | TutorCat - The first realtime Q&A platform 19 | 20 | 21 | 22 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | export default MyApp 53 | 54 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Header/Menu/index.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { NavMenu, PublicMenu } from "./config"; 3 | import { useRouter } from 'next/router'; 4 | import IntlMessages from '../../../utils/IntlMessages'; 5 | 6 | const MenuSecond = (props) => { 7 | const router = useRouter(); 8 | return ( 9 | //
    10 | // { 11 | // NavMenu.map((row, index) => ( 12 | //
  • 13 | // 14 | // 17 | // {row.name} 18 | // 19 | // 20 | //
  • 21 | // )) 22 | // } 23 | 24 | //
25 | <> 26 | 27 | Live Tutor 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | 37 | const MenuPublic = (props) => { 38 | const router = useRouter(); 39 | return ( 40 |
    41 |
  • }> 42 | 43 |
  • 44 | { 45 | PublicMenu.map((row, index) => ( 46 |
  • 50 | 51 | 52 | {row.name} 53 | 54 | 55 |
  • 56 | )) 57 | } 58 | 59 |
60 | ); 61 | } 62 | 63 | export { 64 | MenuSecond, 65 | MenuPublic 66 | }; -------------------------------------------------------------------------------- /client/@meowmeow/components/LanguageSwitcher/index.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from "../../utils/IntlMessages"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { setEng, setVi } from "../../redux/actions/config"; 4 | 5 | export default function LanguageSwitcher() { 6 | const langCode = useSelector((res) => res.Config.langCode); 7 | const dispatch = useDispatch(); 8 | return ( 9 | 33 | ); 34 | } -------------------------------------------------------------------------------- /client/components/OutputCodeFromMe.jsx: -------------------------------------------------------------------------------- 1 | import {useState} from "react"; 2 | import OutputCodeModal from "./OutputCodeModal"; 3 | 4 | export default function OutputCodeFromMe({ content, handleAddCodeFromMe}) { 5 | const [showInputCodeModal, setShowInputCodeModal] = useState(false); 6 | 7 | const handleCloseInputCodeModal = () => { 8 | setShowInputCodeModal(false); 9 | }; 10 | return ( 11 | <> 12 |
13 | 21 | 22 |
23 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/components/OutputCodeFromRemote.jsx: -------------------------------------------------------------------------------- 1 | import {useState} from "react"; 2 | import OutputCodeModal from "./OutputCodeModal"; 3 | 4 | export default function OutputCodeFromMe({ content, handleAddCodeFromMe }) { 5 | const [showInputCodeModal, setShowInputCodeModal] = useState(false); 6 | 7 | const handleCloseInputCodeModal = () => { 8 | setShowInputCodeModal(false); 9 | }; 10 | return ( 11 | <> 12 |
13 | 14 | 22 |
23 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /client/pages/questions/edit/[qid].js: -------------------------------------------------------------------------------- 1 | import Forum from '../../../@meowmeow/components/Layout/Forum' 2 | import QuestionEdit from '../../../@meowmeow/components/QuestionEdit' 3 | import { Axios } from '../../../@meowmeow/modules/apiService/config' 4 | import Error from '../../_error' 5 | import { Heading } from '../../../@meowmeow/modules' 6 | import { loggedIn } from '../../../@meowmeow/authentication' 7 | import SignInPage from '../../account/signin'; 8 | import { getLocalStorage, setLocalStorage } from "../../../@meowmeow/modules" 9 | 10 | const questionEditPage = ({ qDetail }) => { 11 | const authUser = loggedIn() 12 | let user = JSON.parse(getLocalStorage("user", true, null)) 13 | return ( 14 | <> 15 | 16 | {(qDetail === undefined || qDetail === null) ? 17 | : 18 | authUser ? 19 | ((user._id === qDetail.userID._id) 20 | ? ( 21 | 22 | ) 23 | : ) 24 | : } 25 | 26 | ) 27 | } 28 | 29 | // export async function getStaticPaths() { 30 | // let data = await Axios 31 | // .get(`/question/`) 32 | // .then(res => { 33 | // let data = res.data.data 34 | // return data 35 | // }) 36 | // .catch(() => { 37 | // return [] 38 | // } 39 | // ) 40 | // const paths = data.map(question => ({ 41 | // params: { qid: question._id }, 42 | // })); 43 | // return { 44 | // paths, 45 | // fallback: 'blocking' // false or 'blocking' 46 | // }; 47 | // } 48 | 49 | export async function getServerSideProps({ params }) { 50 | let data = await Axios 51 | .get(`/question/${params.qid}/detail`) 52 | .then(res => { 53 | let data = res.data.data 54 | return data 55 | }) 56 | .catch(() => { 57 | return null 58 | } 59 | ) 60 | let qDetail = data 61 | return { 62 | props: { 63 | qDetail: qDetail || null, 64 | } 65 | } 66 | } 67 | 68 | export default questionEditPage; -------------------------------------------------------------------------------- /client/@meowmeow/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import IntlMessages from '../../utils/IntlMessages' 3 | import { TailSpin } from 'react-loader-spinner' 4 | import Link from 'next/link'; 5 | 6 | const Loading = () => { 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 |

14 |
15 |
16 | ) 17 | } 18 | 19 | const Modal = ({ children, openModal, handleClose, handleSubmit, modalType, redirectTo }) => { 20 | const modal = openModal ? "modal modal-open" : "modal" 21 | const submit = (handleSubmit != null) ? "btn btn-sm btn-primary" : "hidden" 22 | const close = (handleClose != null) ? "btn btn-sm btn-ghost" : "hidden" 23 | return ( 24 |
25 |
26 |
27 |
28 | {(modalType == 'loading') ? : children} 29 |
30 |
31 | {(modalType != 'loading') ? 32 | (<> 33 | { 34 | (modalType == null) ? <> 35 | 38 | : <> 39 | } 40 | { 41 | (modalType == 'success') ? <> 42 | 45 | : <> 46 | } 47 | { 48 | (modalType == 'redirect') ? <> 49 | 50 | 53 | 54 | 55 | : <> 56 | } 57 | ) : (<>)} 58 |
59 |
60 |
61 |
62 | ); 63 | } 64 | 65 | 66 | export default Modal; -------------------------------------------------------------------------------- /client/@meowmeow/styles/style.css: -------------------------------------------------------------------------------- 1 | /* width */ 2 | ::-webkit-scrollbar { 3 | width: 8px; 4 | height: 8px 5 | } 6 | 7 | /* Track */ 8 | ::-webkit-scrollbar-track { 9 | background: rgb(202, 202, 202); 10 | } 11 | 12 | /* Handle */ 13 | ::-webkit-scrollbar-thumb { 14 | background: #888; 15 | } 16 | 17 | /* Handle on hover */ 18 | ::-webkit-scrollbar-thumb:hover { 19 | background: #b3b3b3; 20 | } 21 | 22 | .sidebar-menu { 23 | border-right: 1px solid rgb(199 199 199); 24 | } 25 | 26 | .navbar-custom { 27 | min-height: 3rem; 28 | /* border-top: 3px solid #7dd3fc; */ 29 | } 30 | 31 | .section { 32 | margin-top: 1rem; 33 | } 34 | 35 | .btn-no-uppercase { 36 | text-transform: 'none' 37 | } 38 | 39 | .ql-editor { 40 | min-height: 20rem; 41 | } 42 | 43 | .m-input { 44 | border-color: #ccc; 45 | } 46 | 47 | 48 | .m-txt2html { 49 | word-wrap: break-word; 50 | } 51 | 52 | .m-txt2html>p { 53 | padding: 5px 0px 5px 0px; 54 | } 55 | 56 | .m-txt2html>pre { 57 | white-space: pre; 58 | background-color: #f3f3f3; 59 | color: black; 60 | overflow: auto; 61 | padding: 10px; 62 | font-size: 15px; 63 | line-height: 20px; 64 | } 65 | 66 | .m-txt2html>.ql-container.ql-snow { 67 | border: none; 68 | font-size: 0.9rem; 69 | margin: 0px; 70 | padding: 0px; 71 | } 72 | 73 | .m-txt2html>.ql-container.ql-snow>.ql-editor { 74 | margin: 0px; 75 | padding: 0px; 76 | 77 | } 78 | 79 | .m-txt2html img { 80 | width: 70%; 81 | margin: 5px 0px 5px 0px; 82 | display: block; 83 | margin-left: auto; 84 | margin-right: auto; 85 | } 86 | 87 | .m-txt2html>.ql-snow .ql-editor pre.ql-syntax { 88 | background-color: #f3f3f3; 89 | color: black; 90 | overflow: auto; 91 | } 92 | 93 | .m-vote { 94 | display: inline-block; 95 | text-align: center; 96 | } 97 | 98 | .m-btnVote { 99 | cursor: pointer; 100 | } 101 | 102 | .m-pointer { 103 | cursor: pointer; 104 | } 105 | 106 | .m-langSwitcher { 107 | position:relative; 108 | } 109 | 110 | .m-card-content-6 { 111 | display: -webkit-box; 112 | -webkit-line-clamp: 6; 113 | -webkit-box-orient: vertical; 114 | overflow: hidden; 115 | } 116 | 117 | .m-card-content-9 { 118 | display: -webkit-box; 119 | -webkit-line-clamp: 9; 120 | -webkit-box-orient: vertical; 121 | overflow: hidden; 122 | } 123 | 124 | .m-toast { 125 | font-weight: bold; 126 | } 127 | 128 | .m-btn-newques { 129 | float: right; 130 | } -------------------------------------------------------------------------------- /client/pages/tags/[tid].js: -------------------------------------------------------------------------------- 1 | import Forum from '../../@meowmeow/components/Layout/Forum' 2 | import { Heading } from '../../@meowmeow/modules' 3 | import TagsDetail from '../../@meowmeow/components/TagsDetail' 4 | import { Axios } from '../../@meowmeow/modules/apiService/config' 5 | import Error from '../_error' 6 | import Head from 'next/head' 7 | import IntlMessages from '../../@meowmeow/utils/IntlMessages'; 8 | import categories from '../../@meowmeow/components/Tags/catatogies'; 9 | 10 | const tagsDetailPage = ({ tQues, tName }) => { 11 | if (categories.includes(tName)) 12 | return ( 13 | <> 14 | {(tQues !== undefined && tQues !== null) ? 15 | <> 16 | 17 | TutorCat - {tName !== undefined ? tName : ""} 18 | 19 | 20 | 21 | : 22 | } 23 | 24 | {(tQues === undefined || tQues === null) ? : } 25 | 26 | 27 | ) 28 | else 29 | return ( 30 | <> 31 | 32 | 33 | ) 34 | } 35 | 36 | // export async function getStaticPaths() { 37 | // let data = await Axios 38 | // .get(`/question/`) 39 | // .then(res => { 40 | // let data = res.data.data 41 | // return data 42 | // }) 43 | // .catch(() => { 44 | // return [] 45 | // } 46 | // ) 47 | // const paths = data.map(question => ({ 48 | // params: { qid: question._id }, 49 | // })); 50 | // return { 51 | // paths, 52 | // fallback: 'blocking' // false or 'blocking' 53 | // }; 54 | // } 55 | 56 | export async function getServerSideProps({ params }) { 57 | let tQues = await Axios 58 | .get(`/question/catalogue/${params.tid}`) 59 | .then(res => { 60 | let data = res.data.data 61 | return data 62 | }) 63 | .catch(() => { 64 | return null 65 | } 66 | ) 67 | return { 68 | props: { 69 | tQues: tQues || null, 70 | tName: params.tid || null 71 | } 72 | } 73 | } 74 | 75 | export default tagsDetailPage; -------------------------------------------------------------------------------- /client/pages/questions/[qid]/answers/edit/[aid].js: -------------------------------------------------------------------------------- 1 | import Forum from '../../../../../@meowmeow/components/Layout/Forum' 2 | import AnswerEdit from '../../../../../@meowmeow/components/AnswerEdit' 3 | import { Axios } from '../../../../../@meowmeow/modules/apiService/config' 4 | import Error from '../../../../_error' 5 | import Head from 'next/head' 6 | import IntlMessages from '../../../../../@meowmeow/utils/IntlMessages'; 7 | import SignInPage from '../../../../account/signin'; 8 | import { loggedIn } from '../../../../../@meowmeow/authentication' 9 | import { getLocalStorage, setLocalStorage } from "../../../../../@meowmeow/modules" 10 | import { Heading } from '../../../../../@meowmeow/modules' 11 | 12 | const answerEditPage = ({ aDetail, questionId }) => { 13 | const authUser = loggedIn() 14 | let user = JSON.parse(getLocalStorage("user", true, null)) 15 | return ( 16 | <> 17 | 18 | {(aDetail === undefined || aDetail === null) ? 19 | : 20 | authUser ? 21 | ((user._id === aDetail.userID._id) 22 | ? ( 23 | 24 | ) 25 | : ) 26 | : } 27 | 28 | ) 29 | } 30 | 31 | // export async function getStaticPaths() { 32 | // let data = await Axios 33 | // .get(`/question/`) 34 | // .then(res => { 35 | // let data = res.data.data 36 | // return data 37 | // }) 38 | // .catch(() => { 39 | // return [] 40 | // } 41 | // ) 42 | // const paths = data.map(question => ({ 43 | // params: { qid: question._id }, 44 | // })); 45 | // return { 46 | // paths, 47 | // fallback: 'blocking' // false or 'blocking' 48 | // }; 49 | // } 50 | 51 | export async function getServerSideProps({ params }) { 52 | let data = await Axios 53 | .get(`/question/${params.qid}/reply`) 54 | .then(res => { 55 | let data = res.data.data 56 | return data 57 | }) 58 | .catch(() => { 59 | return null 60 | } 61 | ) 62 | let aDetail = data.find(reply => reply._id === params.aid) 63 | return { 64 | props: { 65 | aDetail: aDetail || null, 66 | questionId: params.qid || null, 67 | } 68 | } 69 | } 70 | 71 | export default answerEditPage; -------------------------------------------------------------------------------- /client/pages/questions/[qid].js: -------------------------------------------------------------------------------- 1 | import Forum from '../../@meowmeow/components/Layout/Forum' 2 | import PageLoader from '../../@meowmeow/components/PageComponents/PageLoader' 3 | import dynamic from 'next/dynamic'; 4 | import QuestionDetail from '../../@meowmeow/components/QuestionDetail' 5 | import { Axios } from '../../@meowmeow/modules/apiService/config' 6 | import Error from '../_error' 7 | import { Heading } from "../../@meowmeow/modules" 8 | import Head from "next/head" 9 | 10 | const questionDetailPage = ({ qDetail, qReply }) => { 11 | const { convert } = require('html-to-text'); 12 | const content = convert(qDetail.content, { 13 | selectors: [ { selector: 'img', format: 'skip' } ] 14 | }) 15 | return ( 16 | <> 17 | {(qDetail !== undefined && qDetail !== null) ? 18 | <> 19 | 20 | {qDetail.categories[0] !== undefined ? qDetail.categories[0].category:""} - {qDetail.title} 21 | 22 | 23 | 24 | : 25 | <>} 26 | {(qDetail === undefined || qDetail === null) ? : } 27 | 28 | ) 29 | } 30 | 31 | // export async function getStaticPaths() { 32 | // let data = await Axios 33 | // .get(`/question/`) 34 | // .then(res => { 35 | // let data = res.data.data 36 | // return data 37 | // }) 38 | // .catch(() => { 39 | // return [] 40 | // } 41 | // ) 42 | // const paths = data.map(question => ({ 43 | // params: { qid: question._id }, 44 | // })); 45 | // return { 46 | // paths, 47 | // fallback: 'blocking' // false or 'blocking' 48 | // }; 49 | // } 50 | 51 | export async function getServerSideProps({ params }) { 52 | let data = await Axios 53 | .get(`/question/`) 54 | .then(res => { 55 | let data = res.data.data 56 | return data 57 | }) 58 | .catch(() => { 59 | return null 60 | } 61 | ) 62 | let qDetail = data.find(question => question._id === params.qid); 63 | let qReply = await Axios 64 | .get(`/question/${params.qid}/reply/`) 65 | .then(res => { 66 | let data = res.data.data 67 | return data 68 | }) 69 | .catch(() => { 70 | return null 71 | } 72 | ) 73 | return { 74 | props: { 75 | qDetail: qDetail || null, 76 | qReply: qReply || null 77 | } 78 | } 79 | } 80 | 81 | export default questionDetailPage; -------------------------------------------------------------------------------- /client/@meowmeow/modules/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react' 2 | import dayjs from "dayjs" 3 | import Head from 'next/head' 4 | import GetTranslateText from '../../@meowmeow/utils/GetTranslateText' 5 | 6 | const Heading = ({ title1, title2, description }) => { 7 | return ( 8 | 9 | 10 | {GetTranslateText(title1)} - {GetTranslateText(title2)} 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | 19 | function date2local(pastDate) { 20 | var utc = require('dayjs/plugin/utc') 21 | dayjs.extend(utc) 22 | var localizedFormat = require('dayjs/plugin/localizedFormat') 23 | dayjs.extend(localizedFormat) 24 | const result = dayjs(pastDate).utc('z').format('lll') 25 | return result 26 | } 27 | 28 | function getElapsedTime(pastDate) { 29 | var utc = require('dayjs/plugin/utc') 30 | dayjs.extend(utc) 31 | var customParseFormat = require('dayjs/plugin/customParseFormat') 32 | dayjs.extend(customParseFormat) 33 | const date1 = dayjs(new Date()) 34 | const date2 = dayjs(pastDate).utc('z') 35 | 36 | let [years, months, days] = ["", "", ""]; 37 | 38 | if (date1.diff(date2, 'year') > 0) { 39 | years = `${date1.diff(date2, 'year')}y`; 40 | } 41 | if (date1.diff(date2, 'month') > 0) { 42 | months = `${date1.diff(date2, 'month') % 24}m`; 43 | } 44 | if (date1.diff(date2, 'day') > 0 && date1.diff(date2, 'year') == 0) { 45 | days = `${date1.diff(date2, 'day') % 365}d`; 46 | } 47 | 48 | let response = [years, months, days].filter(Boolean); 49 | 50 | switch (response.length) { 51 | case 3: 52 | response[1] += ""; 53 | response[0] += ","; 54 | break; 55 | case 2: 56 | response[0] += ""; 57 | break; 58 | } 59 | return response.join(" "); 60 | } 61 | 62 | function setLocalStorage(key, json = false, value = 0) { 63 | if (typeof window !== 'undefined') { 64 | if (json) 65 | window.localStorage.setItem(key, JSON.stringify(value)) 66 | else 67 | window.localStorage.setItem(key, value) 68 | } 69 | } 70 | 71 | function getLocalStorage(key, json = false, defaultValue = 0) { 72 | let value = null 73 | if (typeof window !== 'undefined') { 74 | const result = window.localStorage.getItem(key) 75 | if (result == null) { 76 | if (json) { 77 | value = JSON.stringify(defaultValue) 78 | setLocalStorage(key, true, defaultValue) 79 | } 80 | else { 81 | value = defaultValue 82 | setLocalStorage(key, false, defaultValue) 83 | } 84 | } 85 | else { 86 | value = result 87 | } 88 | } else 89 | value = defaultValue 90 | return value 91 | } 92 | 93 | export { 94 | getElapsedTime, 95 | date2local, 96 | getLocalStorage, 97 | setLocalStorage, 98 | Heading 99 | } -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionDetail/Vote/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useAuth } from '../../../authentication/index' 3 | import toast from 'react-hot-toast' 4 | import IntlMessages from '../../../utils/IntlMessages' 5 | import {Axios} from '../../../modules/apiService/config' 6 | 7 | const Vote = ({ questionId, voteIndex, question, id }) => { 8 | // console.log(questionId) 9 | const [vote, setVote] = useState(voteIndex); 10 | const { authUser } = useAuth(); 11 | 12 | const upVote = async () => { 13 | if (authUser){ 14 | if (question) { 15 | let res = await Axios 16 | .post(`/question/${questionId}/up-vote/`) 17 | .then(({data}) => { 18 | if (data.statusCode == 200) 19 | setVote(data.data) 20 | else toast.error() 21 | }) 22 | .catch((error) => { 23 | toast.error() 24 | } 25 | ) 26 | } else { 27 | let res = await Axios 28 | .post(`/question/reply/${id}/up-vote`) 29 | .then(({data}) => { 30 | if (data.statusCode == 200) 31 | setVote(data.data) 32 | else toast.error() 33 | }) 34 | .catch((error) => { 35 | toast.error() 36 | } 37 | ) 38 | } 39 | } else { 40 | toast.error() 41 | } 42 | } 43 | 44 | const downVote = async () => { 45 | if (authUser){ 46 | if (question) { 47 | let res = await Axios 48 | .post(`/question/${questionId}/down-vote`) 49 | .then(({data}) => { 50 | if (data.statusCode == 200) 51 | setVote(data.data) 52 | else toast.error() 53 | }) 54 | .catch((error) => { 55 | toast.error() 56 | } 57 | ) 58 | } else { 59 | let res = await Axios 60 | .post(`/question/reply/${id}/down-vote`) 61 | .then(({data}) => { 62 | if (data.statusCode == 200) 63 | setVote(data.data) 64 | else toast.error() 65 | }) 66 | .catch((error) => { 67 | toast.error() 68 | } 69 | ) 70 | } 71 | } else { 72 | toast.error() 73 | } 74 | } 75 | 76 | return ( 77 |
78 | 81 |

{vote}

82 | 85 |
86 | ); 87 | } 88 | 89 | export default Vote; -------------------------------------------------------------------------------- /client/@meowmeow/components/Header/fullPage/trunkey.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from '../../../utils/IntlMessages'; 2 | import { MenuSecond, MenuPublic } from '../Menu'; 3 | import LanguageSwitcher from '../../LanguageSwitcher'; 4 | import ProfileGroup from '../../ProfileGroup'; 5 | import { loggedIn } from '../../../authentication'; 6 | import Link from 'next/link'; 7 | import { useRouter } from 'next/router'; 8 | 9 | export default function Header({ children }) { 10 | const authUser = loggedIn(); 11 | const router = useRouter(); 12 | return ( 13 |
14 | 15 |
16 | {/* Navbar */} 17 |
18 |
19 | 22 |
23 |
24 | 25 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 | {authUser ? : <> 39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | } 51 |
52 |
53 | {/* Page content here */} 54 |
55 | {children} 56 |
57 |
58 |
59 |
72 |
73 | ); 74 | } -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # TutorCat Template 2 | ##### Using for WebDev Adventure 2022. Based on Next.js and Tailwind, DaisyUI 3 | From sheroanh with Love ❤️ 4 | 5 | ## Instruction manual 6 | #### Before using 7 | You need install all packages in package.json: 8 | - Via **npm**: `npm install` 9 | - Via **yarn**: `yarn` 10 | 11 | #### Development 12 | - Via **npm**: `npm dev` 13 | - Via **yarn**: `yarn dev` 14 | 15 | #### Production 16 | Firstly, building all things before starting: 17 | - Via **npm**: `npm build` 18 | - Via **yarn**: `yarn build` 19 | 20 | And then: 21 | - Via **npm**: `npm start` 22 | - Via **yarn**: `yarn start` 23 | 24 | 25 | ## [IMPORTANT] Directory tree and notes 26 | 27 | ```base 28 | ├── @meowmeow\ 29 | │ │ 30 | │ ├── authentication\ 31 | │ │ │ 32 | │ │ ├── auth-methods\ 33 | │ │ │ │ 34 | │ │ │ └── jwt-auth\ 35 | │ │ │ │ 36 | │ │ │ ├── dist\ 37 | │ │ │ │ └── config.dev.js 38 | │ │ │ │ 39 | │ │ │ ├── config.js 40 | │ │ │ └── index.js 41 | │ │ │ 42 | │ │ │ 43 | │ │ ├── auth-page-wrappers\ 44 | │ │ │ ├── AuthPage.js 45 | │ │ │ └── SecurePage.js 46 | │ │ │ 47 | │ │ └── index.js 48 | │ │ 49 | │ ├── components\ 50 | │ │ │ 51 | │ │ ├── Badge\ 52 | │ │ │ └── index.js 53 | │ │ │ 54 | │ │ ├── Header\ 55 | │ │ │ │ 56 | │ │ │ ├── Menu\ 57 | │ │ │ │ ├── config.js // Config menu here 58 | │ │ │ │ └── index.js 59 | │ │ │ │ 60 | │ │ │ ├── fullPage\ 61 | │ │ │ │ └── index.js 62 | │ │ │ │ 63 | │ │ │ └── withSidebar\ 64 | │ │ │ └── index.js 65 | │ │ │ 66 | │ │ │ 67 | │ │ ├── LanguageSwitcher\ 68 | │ │ │ └── index.js 69 | │ │ │ 70 | │ │ ├── Layout\ 71 | │ │ │ ├── Auth.js // Layout with Header only 72 | │ │ │ └── Forum.js // Layout with Sidebar and Header 73 | │ │ │ 74 | │ │ ├── Loading\ 75 | │ │ │ └── index.js 76 | │ │ │ 77 | │ │ ├── Modal\ 78 | │ │ │ └── index.js 79 | │ │ │ 80 | │ │ ├── PageComponents\ 81 | │ │ │ └── PageLoader.js 82 | │ │ │ 83 | │ │ ├── ProfileGroup\ 84 | │ │ │ │ 85 | │ │ │ ├── Menu\ 86 | │ │ │ │ ├── config.js 87 | │ │ │ │ └── index.js 88 | │ │ │ │ 89 | │ │ │ └── index.js 90 | │ │ │ 91 | │ │ ├── QuestionDetail\ 92 | │ │ │ └── index.js 93 | │ │ │ 94 | │ │ ├── QuestionNew\ 95 | │ │ │ └── index.js 96 | │ │ │ 97 | │ │ └── auth\ 98 | │ │ ├── SignIn.js 99 | │ │ └── SignUp.js 100 | │ │ 101 | │ │ 102 | │ ├── modules\ 103 | │ │ 104 | │ ├── redux\ 105 | │ │ │ 106 | │ │ ├── actions\ 107 | │ │ │ └── lang.js 108 | │ │ │ 109 | │ │ └── reducers\ 110 | │ │ ├── index.js 111 | │ │ └── lang.js 112 | │ │ 113 | │ │ 114 | │ ├── styles\ 115 | │ │ ├── global.css 116 | │ │ ├── style.css // Adding your styles here 117 | │ │ └── tailwind.css 118 | │ │ 119 | │ └── utils\ 120 | │ │ 121 | │ ├── i18n\ 122 | │ │ │ 123 | │ │ ├── dist\ 124 | │ │ │ └── index.dev.js 125 | │ │ │ 126 | │ │ ├── entries\ 127 | │ │ │ ├── en-US.js 128 | │ │ │ └── vi-VN.js 129 | │ │ │ 130 | │ │ ├── locales\ // Adding new messages match with Eng and Vie 131 | │ │ │ ├── en_US.json 132 | │ │ │ └── vi_VN.json 133 | │ │ │ 134 | │ │ │ 135 | │ │ ├── index.js 136 | │ │ └── index.js.bak 137 | │ │ 138 | │ ├── IntlMessages.js // Module use for multilanguage 139 | │ └── LangConfig.js 140 | │ 141 | │ 142 | ├── pages\ // Creating new pages here 143 | │ │ 144 | │ ├── account\ 145 | │ │ ├── signin.js 146 | │ │ └── signup.js 147 | │ │ 148 | │ ├── questions\ 149 | │ │ ├── [qid].js 150 | │ │ ├── index.js 151 | │ │ └── new.js 152 | │ │ 153 | │ ├── _app.js 154 | │ ├── explorer.js 155 | │ └── index.js 156 | │ 157 | ├── public\ 158 | │ │ 159 | │ ├── vendor\ 160 | │ │ └── languageSwitcher.svg 161 | │ │ 162 | │ └── favicon.ico 163 | │ 164 | ├── README.md 165 | ├── package-lock.json 166 | ├── package.json 167 | ├── postcss.config.js 168 | └── tailwind.config.js 169 | ``` 170 | 171 | -------------------------------------------------------------------------------- /client/@meowmeow/components/Header/withSidebar/index.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from '../../../utils/IntlMessages'; 2 | import { MenuSecond, MenuPublic } from '../Menu'; 3 | import LanguageSwitcher from '../../LanguageSwitcher'; 4 | import ProfileGroup from '../../ProfileGroup'; 5 | import { loggedIn } from '../../../authentication'; 6 | import Link from 'next/link'; 7 | 8 | export default function Header({ children }) { 9 | const user = loggedIn(); 10 | return ( 11 |
12 | 13 |
14 | {/* Navbar */} 15 |
16 |
17 | 20 |
21 |
22 | 23 | 27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 | {user ? : <> 37 | 38 | 41 | 42 | 43 | 46 | 47 | 48 | } 49 |
50 |
51 | {/* Page content here */} 52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 | {children} 60 |
61 |
62 |
63 |
64 |
65 |
66 |
81 |
82 | ); 83 | } -------------------------------------------------------------------------------- /server/src/api/auth/auth.service.js: -------------------------------------------------------------------------------- 1 | const User = require('../../models/userModel'); 2 | const { AppError } = require('../../common/errors/AppError'); 3 | const bcrypt = require('bcrypt'); 4 | const sendEmail = require('../../common/email/email'); 5 | const jwt = require('jsonwebtoken'); 6 | const uuidv4 = require('uuidv4'); 7 | 8 | module.exports = { 9 | signUp: async ({ name, email, password }) => { 10 | try { 11 | let identical = await User.findOne({ email }); 12 | console.log(identical); 13 | if (identical) { 14 | throw new AppError(403, 'User already existed'); 15 | } 16 | let salt = await bcrypt.genSalt(10); 17 | let hashPassword = await bcrypt.hash(password, salt); 18 | await User.create({ 19 | name: name, 20 | email: email, 21 | password: hashPassword, 22 | }); 23 | return { 24 | statusCode: 200, 25 | message: 'Account created successfully', 26 | }; 27 | } catch (error) { 28 | throw new AppError(500, error.message); 29 | } 30 | }, 31 | signIn: async ({ email, password }) => { 32 | try { 33 | let filter = await User.find({ email: email }).select('password'); 34 | if (filter.length === 1) { 35 | if (await bcrypt.compare(password, filter[0].password)) { 36 | let token = jwt.sign( 37 | { 38 | userId: filter[0]._id, 39 | }, 40 | process.env.JWT_SECRET_KEY, 41 | { 42 | expiresIn: '30d', 43 | }, 44 | ); 45 | return { 46 | statusCode: 200, 47 | message: 'Succesfully logged in', 48 | token: token, 49 | }; 50 | } else { 51 | throw new AppError(403, 'Wrong password'); 52 | } 53 | } else { 54 | throw new AppError(404, 'User not found'); 55 | } 56 | } catch (error) { 57 | throw new AppError(500, error.message); 58 | } 59 | }, 60 | forgetPassword: async ({ email }) => { 61 | try { 62 | let valid = await User.findOne({ email: email }); 63 | if (!valid) { 64 | throw new AppError(404, 'User not found'); 65 | } 66 | await sendEmail(email, uuidv4); 67 | return { 68 | statusCode: 200, 69 | message: 'Mail sent successfully', 70 | }; 71 | } catch (error) { 72 | throw new AppError(500, error.message); 73 | } 74 | }, 75 | resetPassword: async ({ userId, resetToken, newPassword }) => { 76 | let validToken = await User.findOne({ passwordResetToken: resetToken }); 77 | if (!validToken) { 78 | throw new AppError(400, 'Invalid token'); 79 | } 80 | let salt = await bcrypt.genSalt(10); 81 | let hashPassword = await bcrypt.hash(newPassword, salt); 82 | await User.findOneAndUpdate( 83 | { _id: userId }, 84 | { password: hashPassword, passwordChangedAt: Date.now() }, 85 | { new: true }, 86 | ); 87 | }, 88 | 89 | updatePassword: async (user, { oldPassword, newPassword }) => { 90 | try { 91 | let info = await User.findById(user.id).select('password'); 92 | if (!(await bcrypt.compare(oldPassword, info.password))) { 93 | throw new AppError(403, 'Wrong old password'); 94 | } 95 | let salt = await bcrypt.genSalt(10); 96 | let hashPassword = await bcrypt.hash(newPassword, salt); 97 | info.password = hashPassword; 98 | info.passwordChangedAt = Date.now(); 99 | await info.save(); 100 | let token = jwt.sign( 101 | { 102 | userId: info.id, 103 | }, 104 | process.env.JWT_SECRET_KEY, 105 | { 106 | expiresIn: '30d', 107 | }, 108 | ); 109 | return { 110 | statusCode: 200, 111 | message: 'Successfully changed password', 112 | token: token, 113 | }; 114 | } catch (error) { 115 | throw new AppError(500, error.message); 116 | } 117 | }, 118 | }; 119 | -------------------------------------------------------------------------------- /server/src/api/room/room.controller.js: -------------------------------------------------------------------------------- 1 | const room = require('../../models/roomModel'); 2 | 3 | class RoomController { 4 | getRoom(req, res) { 5 | var filter = {}; 6 | filter.roomID = req.params.roomID; 7 | room 8 | .findOne(filter) 9 | .then((room) => { 10 | console.log(filter); 11 | res.send(room); 12 | }) 13 | .catch(() => res.status(500).send({ message: "Cannot get rooms" })); 14 | } 15 | 16 | getAllAvailableRooms(req, res) { 17 | room 18 | .find({ userCount : {$gt : 0}}) 19 | .then((room) => { 20 | res.status(200).send(room); 21 | }) 22 | .catch(() => res.status(500).send({ message: "Cannot get all rooms" })); 23 | } 24 | 25 | addRoom(req, res) { 26 | const newRoom = new room(req.body); 27 | newRoom 28 | .save() 29 | .then(() => { 30 | res.status(200).send({ message: "created" }); 31 | }) 32 | .catch((err) => { 33 | console.log(err); 34 | res.status(503).send({ message: "fail create room" }); 35 | }); 36 | } 37 | 38 | modifyRoom(req, res) { 39 | const roomObject = req.body; 40 | const roomId = req.params.roomId; 41 | room 42 | .findOneAndUpdate({ _id: roomId }, roomObject, { new: true }) 43 | .then((room) => { 44 | res.send(room); 45 | }) 46 | .catch(() => res.status(503).send({ message: "Cannot modify room" })); 47 | } 48 | 49 | deleteRoom(req, res) { 50 | const roomID = req.params.roomID; 51 | room 52 | .findOneAndDelete({ roomID: roomID }) 53 | .then(() => res.send({ message: `delete ${roomID}` })) 54 | .catch((err) => { 55 | console.log(err); 56 | res.status(503).send({ message: "Cannot delete room" }); 57 | }); 58 | } 59 | 60 | deleteRoomIfEmpty(req, res) { 61 | const roomID = req.params.roomID; 62 | room 63 | .findOneAndDelete({ roomID: roomID, userCount: 0 }) 64 | .then(() => res.send({ message: `delete ${roomID}` })) 65 | .catch((err) => { 66 | console.log(err); 67 | res.status(503).send({ message: "Cannot delete room" }); 68 | }); 69 | } 70 | 71 | increaseUserCount(req, res) { 72 | const roomID = req.params.roomID; 73 | const userID = req.params.userID; 74 | room 75 | .findOne({ roomID: roomID }) 76 | .then((_room) => { 77 | if (!_room) { 78 | res.status(404).send({ message: "Cannot join room " + roomID }); 79 | return; 80 | } 81 | 82 | if (_room.userCount >= 2) { 83 | res.status(403).send({ message: "Cannot join room " + roomID }); 84 | return; 85 | } 86 | 87 | if (_room.userCount == 0) { 88 | room 89 | .findOneAndUpdate( 90 | { roomID: roomID }, 91 | { $inc: { userCount: 1 }, $set: { userID1: userID } }, 92 | { new: true } 93 | ) 94 | .then((room) => { 95 | res.status(200).send(room); 96 | }) 97 | .catch(() => { 98 | res.status(503).send({ message: "Cannot join room " + roomID }); 99 | }); 100 | return; 101 | } 102 | 103 | if (_room.userCount == 1) { 104 | room 105 | .findOneAndUpdate( 106 | { roomID: roomID }, 107 | { $inc: { userCount: 1 }, $set: { userID2: userID } }, 108 | { new: true } 109 | ) 110 | .then((room) => { 111 | res.status(200).send(room); 112 | }) 113 | .catch(() => { 114 | res.status(503).send({ message: "Cannot join room " + roomID }); 115 | }); 116 | return; 117 | } 118 | }) 119 | 120 | .catch((err) => { 121 | console.log(err); 122 | res.status(503).send({ message: "Cannot join room " + roomID }); 123 | }); 124 | } 125 | 126 | decreaseUserCount(req, res) { 127 | const roomID = req.params.roomID; 128 | 129 | room 130 | .findOne({ roomID: roomID }) 131 | .then((_room) => { 132 | if (_room.userCount <= 0) { 133 | room 134 | .findOneAndDelete({ roomID: roomID }) 135 | .then(() => res.status(200).send(OK)); 136 | } else { 137 | room 138 | .findOneAndUpdate( 139 | { roomID: roomID }, 140 | { $inc: { userCount: 1 } }, 141 | { new: true } 142 | ) 143 | .then((room) => { 144 | res.status(200).send(room); 145 | }) 146 | .catch(() => { 147 | res.status(503).send({ message: "Cannot join room " + roomID }); 148 | }); 149 | } 150 | }) 151 | .catch((err) => { 152 | console.log(err); 153 | res.status(503).send({ message: "Cannot join room " + roomID }); 154 | }); 155 | } 156 | } 157 | 158 | module.exports = new RoomController(); 159 | -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionDetail/Detail/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Component, useEffect } from 'react' 2 | import 'react-quill/dist/quill.snow.css' 3 | import hljs from 'highlight.js' 4 | import 'highlight.js/styles/base16/solarized-light.css' 5 | import parse from 'html-react-parser' 6 | import Vote from '../Vote' 7 | import { date2local, getLocalStorage } from '../../../modules' 8 | import IntlMessages from '../../../utils/IntlMessages' 9 | import { loggedIn } from '../../../authentication/index' 10 | import toast from 'react-hot-toast' 11 | import {CopyToClipboard} from 'react-copy-to-clipboard'; 12 | 13 | hljs.configure({ 14 | languages: ['javascript', 'ruby', 'python', 'rust', 'c++', 'undefined'], 15 | }) 16 | 17 | class Quill2Html extends Component { 18 | constructor({ detail }) { 19 | super({ detail }) 20 | this.detail = detail 21 | this.updateCodeSyntaxHighlighting = this.updateCodeSyntaxHighlighting.bind(this) 22 | } 23 | 24 | componentDidMount() { 25 | this.updateCodeSyntaxHighlighting(); 26 | } 27 | 28 | componentDidUpdate() { 29 | this.updateCodeSyntaxHighlighting(); 30 | } 31 | 32 | updateCodeSyntaxHighlighting = () => { 33 | document.querySelectorAll("pre").forEach(block => { 34 | hljs.highlightBlock(block); 35 | }); 36 | }; 37 | 38 | render() { 39 | 40 | return ( 41 | <> 42 |
43 | {parse(this.detail)} 44 |
45 | 46 | ) 47 | } 48 | } 49 | 50 | const Share = ({questionId}) => { 51 | const copied = () => { 52 | toast.success() 53 | } 54 | let url = window.location.protocol + "//" + window.location.host +"/questions/" + questionId; 55 | return ( 56 | copied()}> 58 | 59 | 60 | ) 61 | } 62 | 63 | const Detail = ({ detail, voteIndex, time, id, questionId, user, question }) => { 64 | const authUser = loggedIn() 65 | let userDetail = JSON.parse(getLocalStorage("user", true, null)) 66 | const edit = ({question, id, questionId}) => { 67 | if (authUser){ 68 | if (user._id === userDetail._id) 69 | { 70 | const url = question ? `/questions/edit/${questionId}`: `/questions/${questionId}/answers/edit/${id}` 71 | window.location.href = url 72 | } 73 | else toast.error() 74 | } 75 | else toast.error() 76 | } 77 | return ( 78 | <> 79 |
80 |
81 | 82 |
83 |
84 |
85 | 86 |
87 |
88 |
89 |
90 | {user.name}/ 91 |
92 |
93 |
94 |

{user.name}

95 |
96 |

97 |

{date2local(time)}

98 |
99 |
100 |
101 |
102 | 110 |
111 |
112 |
113 | 114 | )} 115 | 116 | export default Detail 117 | -------------------------------------------------------------------------------- /server/src/api/question/question.controller.js: -------------------------------------------------------------------------------- 1 | const { AppError } = require('../../common/errors/AppError'); 2 | const questionService = require('./question.service'); 3 | // const upload = require('../../common/uploadCloudinary/uploadCloudinary'); 4 | module.exports = { 5 | uploadImage: async (req, res, next) => { 6 | try { 7 | if (req.file) { 8 | res.status(200).json({ 9 | statusCode: 200, 10 | message: 'Uploaded successfully', 11 | data: req.file.path, 12 | }); 13 | } 14 | next(new AppError(500, 'Upload failed')); 15 | } catch (error) { 16 | next(error); 17 | } 18 | }, 19 | getAllQuestion: async (req, res, next) => { 20 | try { 21 | let DTO = await questionService.getAllQuestion(); 22 | res.status(200).json(DTO); 23 | } catch (error) { 24 | next(error); 25 | } 26 | }, 27 | addQuestion: async (req, res, next) => { 28 | try { 29 | let DTO = await questionService.addQuestion(req.user.id, req.body); 30 | res.status(200).json(DTO); 31 | } catch (error) { 32 | console.log(error); 33 | next(error); 34 | } 35 | }, 36 | getQuestionWithID: async (req, res, next) => { 37 | try { 38 | let DTO = await questionService.getQuestionWithID(req.params.id); 39 | res.status(200).json(DTO); 40 | } catch (error) { 41 | next(error); 42 | } 43 | }, 44 | getQuestionWithUserID: async (req, res, next) => { 45 | try { 46 | let DTO = await questionService.getQuestionWithID(req.user.id); 47 | res.status(200).json(DTO); 48 | } catch (error) { 49 | next(error); 50 | } 51 | }, 52 | deleteQuestion: async (req, res, next) => { 53 | try { 54 | let DTO = await questionService.deleteQuestion(req.user.id, req.params.id); 55 | res.status(200).json(DTO); 56 | } catch (error) { 57 | next(error); 58 | } 59 | }, 60 | modifyQuestion: async (req, res, next) => { 61 | try { 62 | let DTO = await questionService.modifyQuestion(req.user.id, req.params.id, req.body); 63 | res.status(200).json(DTO); 64 | } catch (error) { 65 | console.log(error); 66 | next(error); 67 | } 68 | }, 69 | upVoteQuestion: async (req, res, next) => { 70 | try { 71 | let DTO = await questionService.upVoteQuestion(req.user.id, req.params.id); 72 | res.status(200).json(DTO); 73 | } catch (error) { 74 | next(error); 75 | } 76 | }, 77 | downVoteQuestion: async (req, res, next) => { 78 | try { 79 | let DTO = await questionService.downVoteQuestion(req.user.id, req.params.id); 80 | res.status(200).json(DTO); 81 | } catch (error) { 82 | next(error); 83 | } 84 | }, 85 | getAllReply: async (req, res, next) => { 86 | try { 87 | let DTO = await questionService.getAllReply(req.params.id); 88 | res.status(200).json(DTO); 89 | } catch (error) { 90 | next(error); 91 | } 92 | }, 93 | deleteReply: async (req, res, next) => { 94 | try { 95 | let DTO = await questionService.deleteReply(req.user.id, req.params.id); 96 | res.status(200).json(DTO); 97 | } catch (error) { 98 | next(error); 99 | } 100 | }, 101 | upVoteReply: async (req, res, next) => { 102 | try { 103 | let DTO = await questionService.upVoteReply(req.user.id, req.params.id); 104 | res.status(200).json(DTO); 105 | } catch (error) { 106 | next(error); 107 | } 108 | }, 109 | downVoteReply: async (req, res, next) => { 110 | try { 111 | let DTO = await questionService.downVoteReply(req.user.id, req.params.id); 112 | res.status(200).json(DTO); 113 | } catch (error) { 114 | next(error); 115 | } 116 | }, 117 | addReply: async (req, res, next) => { 118 | try { 119 | let DTO = await questionService.addReply(req.user.id, req.params.id, req.body); 120 | res.status(200).json(DTO); 121 | } catch (error) { 122 | next(error); 123 | } 124 | }, 125 | modifyReply: async (req, res, next) => { 126 | try { 127 | let DTO = await questionService.modifyReply(req.user.id, req.params.id, req.body); 128 | res.status(200).json(DTO); 129 | } catch (error) { 130 | next(error); 131 | } 132 | }, 133 | getCatalogue: async (req, res, next) => { 134 | try { 135 | let DTO = await questionService.getCatalogue(); 136 | res.status(200).json(DTO); 137 | } catch (error) { 138 | next(error); 139 | } 140 | }, 141 | getQuestionWithCategory: async (req, res, next) => { 142 | try { 143 | let DTO = await questionService.getQuestionWithCategory(req.params.category); 144 | res.status(200).json(DTO); 145 | } catch (error) { 146 | next(error); 147 | } 148 | }, 149 | }; 150 | -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionDetail/CommentBox/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }); 4 | import 'react-quill/dist/quill.snow.css'; 5 | import { Axios } from '../../../modules/apiService/config' 6 | import toast from 'react-hot-toast' 7 | import 'react-quill/dist/quill.snow.css'; 8 | import IntlMessages from '../../../utils/IntlMessages' 9 | 10 | class CommentBox extends React.Component { 11 | constructor(props) { 12 | super(props); 13 | this.state = { editorHtml: '', questionId: props.questionId }; 14 | this.handleChange = this.handleChange.bind(this); 15 | this.handleSubmit = this.handleSubmit.bind(this); 16 | } 17 | 18 | handleChange(html) { 19 | this.setState({ editorHtml: html }); 20 | } 21 | 22 | handleSubmit = () => { 23 | let question = { 24 | "content": this.state.editorHtml, 25 | } 26 | toast.loading() 27 | Axios 28 | .post(`/question/${this.state.questionId}/reply/add/`, question, { 29 | headers: { 30 | 'Content-Type': 'application/json' 31 | } 32 | }) 33 | .then(({ data }) => { 34 | if (data.statusCode == "200") { 35 | toast.dismiss() 36 | this.setState({editorHtml: '\n'}) 37 | this.props.updateComment() 38 | } 39 | else 40 | toast.error(data.message) 41 | }) 42 | .catch((error) => { 43 | toast.error(error.message) 44 | } 45 | ) 46 | } 47 | 48 | imageHandler() { 49 | const input = document.createElement('input'); 50 | 51 | input.setAttribute('type', 'file'); 52 | input.setAttribute('accept', 'image/*'); 53 | input.click(); 54 | 55 | input.onchange = async () => { 56 | const file = input.files[0]; 57 | const formData = new FormData(); 58 | 59 | formData.append('photo', file); 60 | 61 | // Save current cursor state 62 | const range = this.quill.getSelection(true); 63 | 64 | // Insert temporary loading placeholder image 65 | this.quill.insertEmbed(range.index, 'image', ""); 66 | 67 | // Move cursor to right side of image (easier to continue typing) 68 | this.quill.setSelection(range.index + 1); 69 | 70 | const res = await Axios 71 | .post('/question/upload', formData, { 72 | headers: { 73 | 'Content-Type': 'multipart/form-data' 74 | } 75 | }) 76 | .then(({ data }) => { 77 | if (data.statusCode == "200") { 78 | // console.log(data.data) 79 | return data.data; 80 | } 81 | else { 82 | toast.error(data.message); 83 | } 84 | }) 85 | .catch(function (error) { 86 | toast.error(error.message); 87 | }); 88 | 89 | this.quill.deleteText(range.index, 1); 90 | 91 | // Insert uploaded image 92 | // this.quill.insertEmbed(range.index, 'image', res.body.image); 93 | this.quill.insertEmbed(range.index, 'image', res); 94 | }; 95 | } 96 | 97 | render() { 98 | 99 | return ( 100 | <> 101 |
102 |

103 | { 105 | this.quill = el; 106 | }} 107 | value={this.state.editorHtml} 108 | onChange={this.handleChange} 109 | placeholder={this.props.placeholder} 110 | modules={{ 111 | toolbar: { 112 | container: [ 113 | [{ header: '1' }, { header: '2' }, { header: [3, 4, 5, 6] }], 114 | ['bold', 'italic', 'underline', 'strike', 'blockquote'], 115 | [{ list: 'ordered' }, { list: 'bullet' }], 116 | 117 | ['link', 'image', 'video'], 118 | ['clean'], 119 | ['code-block'] 120 | ], 121 | handlers: { 122 | image: this.imageHandler 123 | } 124 | } 125 | }} 126 | /> 127 |
128 | 131 |
132 |
133 | 134 | 135 | ); 136 | } 137 | } 138 | 139 | export default CommentBox; -------------------------------------------------------------------------------- /client/@meowmeow/authentication/auth-methods/jwt-auth/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useEffect, useState } from 'react' 3 | import { Axios } from '../../../modules/apiService/config' 4 | import toast from 'react-hot-toast' 5 | import { useSelector, useDispatch } from "react-redux"; 6 | import { signIn, signOut } from "../../../redux/actions/config"; 7 | import { getLocalStorage, setLocalStorage } from "../../../modules" 8 | import IntlMessages from '../../../utils/IntlMessages' 9 | 10 | export const useProvideAuth = () => { 11 | const dispatch = useDispatch(); 12 | const [authUser, setAuthUser] = useState(null); 13 | const [error, setError] = useState(''); 14 | const [message, setMessage] = useState(''); 15 | const [loadingAuthUser, setLoadingAuthUser] = useState(true); 16 | const [isLoading, setLoading] = useState(false); 17 | 18 | const fetchStart = () => { 19 | setLoading(true); 20 | setError(''); 21 | }; 22 | 23 | const fetchSuccess = () => { 24 | setLoading(false); 25 | setError(''); 26 | }; 27 | 28 | const fetchError = (error) => { 29 | setLoading(false); 30 | setError(error); 31 | }; 32 | 33 | const fetchNoti = (txt) => { 34 | setLoading(false); 35 | setMessage(txt); 36 | }; 37 | 38 | const messageSignin = (code) => { 39 | switch (code) { 40 | default: 41 | return ("Error!"); 42 | } 43 | }; 44 | 45 | const messageSignup = (code) => { 46 | switch (code) { 47 | default: 48 | return ("Error!"); 49 | } 50 | }; 51 | 52 | const messageChange = (code) => { 53 | switch (code) { 54 | default: 55 | return ("Error!"); 56 | } 57 | }; 58 | 59 | const bypassLogin = (user, callbackFun) => { 60 | setAuthUser(true); 61 | } 62 | 63 | const userLogin = (user, callbackFun) => { 64 | fetchStart(); 65 | // console.log(user) 66 | Axios 67 | .post('/auth/sign-in/', user) 68 | .then(({ data }) => { 69 | if (data.statusCode == "200") { 70 | toast.success() 71 | dispatch(signIn()) 72 | setAuthUser(true) 73 | setLoadingAuthUser(false) 74 | getInfo() 75 | } 76 | else { 77 | toast.error(); 78 | } 79 | }) 80 | .catch(function (error) { 81 | toast.error(); 82 | }) 83 | } 84 | 85 | const userSignup = (user) => { 86 | fetchStart(); 87 | Axios 88 | .post('/auth/sign-up/', user) 89 | .then(({ data }) => { 90 | fetchSuccess(); 91 | if (data.statusCode == "200") { 92 | toast.success(); 93 | const data = { 94 | email: user.email, 95 | password: user.password, 96 | } 97 | userLogin(data) 98 | } 99 | else { 100 | toast.error(); 101 | } 102 | }) 103 | .catch(function (error) { 104 | toast.error(); 105 | }) 106 | }; 107 | 108 | 109 | const sendPasswordResetEmail = (email, callbackFun) => { 110 | fetchStart(); 111 | 112 | setTimeout(() => { 113 | fetchSuccess(); 114 | //if (callbackFun) callbackFun(); 115 | }, 300); 116 | }; 117 | 118 | const confirmPasswordReset = (code, password, callbackFun) => { 119 | fetchStart(); 120 | 121 | setTimeout(() => { 122 | fetchSuccess(); 123 | //if (callbackFun) callbackFun(); 124 | }, 300); 125 | }; 126 | 127 | const renderSocialMediaLogin = () => null; 128 | 129 | const userSignOut = () => { 130 | Axios 131 | .delete('/auth/sign-out/') 132 | .then(() => { 133 | dispatch(signOut()) 134 | setAuthUser(false) 135 | setLocalStorage("user", true, null) 136 | toast.success() 137 | }) 138 | .catch(function (error) { 139 | toast.error() 140 | }); 141 | }; 142 | 143 | const getInfo = () => { 144 | Axios 145 | .get('/user/get-info') 146 | .then(({ data }) => { 147 | if (data.statusCode == "200") { 148 | dispatch(signIn()) 149 | setLocalStorage("user", true, data.info) 150 | setAuthUser(true) 151 | setLoadingAuthUser(false) 152 | } 153 | else { 154 | dispatch(signOut()) 155 | setAuthUser(false) 156 | setLoadingAuthUser(true) 157 | } 158 | }) 159 | .catch(function (error) { 160 | dispatch(signOut()) 161 | setAuthUser(false) 162 | setLoadingAuthUser(true) 163 | // toast.error(error.message) 164 | }); 165 | }; 166 | 167 | 168 | const changeInfo = (user, callbackFun) => { 169 | 170 | }; 171 | 172 | const getAuthUser = () => { 173 | 174 | }; 175 | 176 | // Subscribe to user on mount 177 | // Because this sets state in the callback it will cause any ... 178 | // ... component that utilizes this hook to re-render with the ... 179 | // ... latest auth object. 180 | 181 | useEffect(() => { 182 | getInfo(); 183 | }, []); 184 | 185 | // Return the user object and auth methods 186 | return { 187 | loadingAuthUser, 188 | isLoading, 189 | authUser, 190 | error, 191 | bypassLogin, 192 | setError, 193 | message, 194 | setMessage, 195 | setAuthUser, 196 | getAuthUser, 197 | userLogin, 198 | userSignup, 199 | userSignOut, 200 | getInfo, 201 | changeInfo, 202 | renderSocialMediaLogin, 203 | sendPasswordResetEmail, 204 | confirmPasswordReset, 205 | }; 206 | }; 207 | -------------------------------------------------------------------------------- /server/src/api/compiler/compiler.controller.js: -------------------------------------------------------------------------------- 1 | const request = require("request"); 2 | const axios = require('axios'); 3 | 4 | class CompilerController { 5 | createSubmission(req, res, next) { 6 | var {source, input, language} = req.body; 7 | var compilerId; 8 | 9 | if (source == "") { 10 | res.status(403).send("Invalid source code"); 11 | return; 12 | } 13 | 14 | switch(language) { 15 | case "cpp": 16 | compilerId = 44; 17 | break; 18 | case "javascript": 19 | compilerId = 112; 20 | break; 21 | case "python": 22 | compilerId = 116; 23 | break; 24 | case "csharp": 25 | compilerId = 27; 26 | break; 27 | case "java": 28 | compilerId = 10 29 | break; 30 | default: 31 | compilerId = -1; 32 | // code block 33 | }; 34 | 35 | //Do đối tượng console không tốn tại trong Rhino nên sẽ sửa thì xíu ở chỗ này cho dễ sử dụng 36 | if (source.includes('console.log')){ 37 | source = source.replace('console.log', 'print'); 38 | res.locals.warning = "Current JS compiler doesn't have console object. Therefore, we replace console.log with print"; 39 | } 40 | 41 | 42 | if (compilerId == -1){ 43 | res.status(403); 44 | return; 45 | }; 46 | 47 | const submissionData = { 48 | compilerId: compilerId, 49 | source: source, 50 | input: input, 51 | }; 52 | 53 | request.post( 54 | { 55 | url: process.env.API_COMPILER_ADDRESS + "/submissions?access_token=" + process.env.API_COMPILER_TOKEN, 56 | form: submissionData, 57 | }, 58 | (err, response) => { 59 | if (err) { 60 | res.status(503); 61 | return; 62 | } 63 | 64 | if (response.statusCode === 201) { 65 | const body = JSON.parse(response.body); 66 | res.locals.submissionId = body.id; 67 | next(); 68 | return; 69 | } 70 | 71 | if (response.statusCode === 401) { 72 | res.status(401).send("Token might be expired"); 73 | return; 74 | } 75 | 76 | if (response.statusCode === 402) { 77 | res.status(402).send("Unable to submit, please try again"); 78 | return; 79 | } 80 | 81 | if (response.statusCode === 400) { 82 | const error = JSON.parse(response.body); 83 | res.status(202).send(error); 84 | } 85 | } 86 | ); 87 | } 88 | 89 | getSubmission(req, res, next) { 90 | const submissionId = res.locals.submissionId || req.params.submissionId; 91 | 92 | if (!submissionId) { 93 | res.status(404).send("Please include submission ID"); 94 | return; 95 | } 96 | 97 | //Sau khi post code tới server xử lý server sẽ nhận code và xử lý, khi get output sẽ nhận dc trạng thái code : 98 | //Nếu code đang chạy phía server compile thì executing == true 99 | //Nếu code đã thực thi xong thì executing == false 100 | //Nhiệm vụ hiện tại là sau khi post code request thử code đã execute chưa nếu chưa thì 2 giây sau gọi api tip 101 | 102 | //Đây là số lần request kiểm tra 103 | var requestTimes = 0; 104 | 105 | const getSubmissionOutput = () => { 106 | console.log("request submission"); 107 | request.get( 108 | { 109 | url: 110 | process.env.API_COMPILER_ADDRESS + 111 | "/submissions/" + 112 | submissionId + 113 | "?access_token=" + 114 | process.env.API_COMPILER_TOKEN, 115 | }, 116 | async (err, response) => { 117 | if (err) { 118 | res.status(503); 119 | return; 120 | } 121 | 122 | if (response.statusCode === 200) { 123 | const body = JSON.parse(response.body); 124 | if (body.executing == true) { 125 | //Nếu đã request kiểm tra 15 lần mà vẫn executing thì bỏ qua code đó 126 | if (requestTimes == 15) { 127 | res 128 | .status(403) 129 | .send("Invalid source code, source code time out!!"); 130 | return; 131 | } 132 | setTimeout(() => { 133 | //Sau 2 giây thử gọi đệ quy request lại 134 | getSubmissionOutput(++requestTimes); 135 | }, 2000); 136 | return; 137 | } else { 138 | //Có lỗi 139 | if (body.result.streams.error){ 140 | const status = body.result.status; 141 | const {data} = await axios.get(body.result.streams.error.uri); 142 | res.status(202).send({status, error : data, warning : res.locals.warning}); 143 | return; 144 | } 145 | 146 | //Có output result 147 | if (body.result.streams.output){ 148 | const status = body.result.status; 149 | const {data} = await axios.get(body.result.streams.output.uri); 150 | const time = body.result.time; 151 | const memory = body.result.memory; 152 | res.status(200).send({status, output: data, time, memory, warning : res.locals.warning}); 153 | return; 154 | } 155 | else { 156 | const status = body.result.status; 157 | res.status(202).send({status, error : status.name}); 158 | } 159 | } 160 | } 161 | 162 | if (response.statusCode === 401) { 163 | res.status(401).send("Token might be expired"); 164 | return; 165 | } 166 | 167 | if (response.statusCode === 402) { 168 | res.status(402).send("Unable to submit, please try again"); 169 | return; 170 | } 171 | 172 | if (response.statusCode === 400) { 173 | const error = JSON.parse(response.body); 174 | res.status(202).send(error); 175 | } 176 | } 177 | ); 178 | }; 179 | 180 | getSubmissionOutput(++requestTimes); 181 | } 182 | } 183 | 184 | module.exports = new CompilerController(); 185 | -------------------------------------------------------------------------------- /client/@meowmeow/components/AnswerEdit/index.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | const ReactQuill = dynamic(() => import('react-quill'), { ssr: false }); 4 | import 'react-quill/dist/quill.snow.css'; 5 | import { Axios } from '../../modules/apiService/config' 6 | import toast from 'react-hot-toast' 7 | import 'react-quill/dist/quill.snow.css'; 8 | import IntlMessages from '../../utils/IntlMessages' 9 | import Select from 'react-select' 10 | import Modal from '../Modal' 11 | 12 | class MyComponent extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { editorHtml: props.data.content, openModal: false, modalType: null, modalContent: '', questionId: props.questionId, redirectTo: '', id: props.data._id }; 16 | this.handleChange = this.handleChange.bind(this); 17 | this.handleSubmitModal = this.handleSubmitModal.bind(this) 18 | this.handleCloseModal = this.handleCloseModal.bind(this) 19 | } 20 | 21 | handleChange(html) { 22 | this.setState({ editorHtml: html }); 23 | } 24 | 25 | handleCloseModal = () => { 26 | this.setState({ openModal: false }) 27 | } 28 | 29 | handleSubmitModal = () => { 30 | } 31 | 32 | handleSubmit = () => { 33 | this.setState({ openModal: true, modalType: 'loading' }) 34 | let question = { 35 | "content": this.state.editorHtml, 36 | } 37 | Axios 38 | .patch(`/question/reply/${this.state.id}/modify`, question, { 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | } 42 | }) 43 | .then(({ data }) => { 44 | let res = data.data 45 | let content = (res !== null) ? : 46 | this.setState({ openModal: true, modalType: (res !== null) ? "redirect" : null, modalContent: content}) 47 | }) 48 | .catch(() => { 49 | let res = null 50 | let content = (res !== null) ? : 51 | this.setState({ openModal: true, modalType: (res !== null) ? "redirect" : null, modalContent: content}) 52 | } 53 | ) 54 | } 55 | 56 | imageHandler() { 57 | const input = document.createElement('input'); 58 | 59 | input.setAttribute('type', 'file'); 60 | input.setAttribute('accept', 'image/*'); 61 | input.click(); 62 | 63 | input.onchange = async () => { 64 | const file = input.files[0]; 65 | const formData = new FormData(); 66 | 67 | formData.append('photo', file); 68 | 69 | // Save current cursor state 70 | const range = this.quill.getSelection(true); 71 | 72 | // Insert temporary loading placeholder image 73 | this.quill.insertEmbed(range.index, 'image', ""); 74 | 75 | // Move cursor to right side of image (easier to continue typing) 76 | this.quill.setSelection(range.index + 1); 77 | 78 | const res = await Axios 79 | .post('/question/upload', formData, { 80 | headers: { 81 | 'Content-Type': 'multipart/form-data' 82 | } 83 | }) 84 | .then(({ data }) => { 85 | if (data.statusCode == "200") { 86 | // console.log(data.data) 87 | return data.data; 88 | } 89 | else { 90 | toast.error(data.message); 91 | } 92 | }) 93 | .catch(function (error) { 94 | toast.error(error.message); 95 | }); 96 | 97 | this.quill.deleteText(range.index, 1); 98 | 99 | // Insert uploaded image 100 | // this.quill.insertEmbed(range.index, 'image', res.body.image); 101 | this.quill.insertEmbed(range.index, 'image', res); 102 | }; 103 | } 104 | 105 | render() { 106 | return ( 107 | <> 108 | 114 |

{this.state.modalContent}

115 |
116 |

117 | 118 |

119 | { 121 | this.quill = el; 122 | }} 123 | value={this.state.editorHtml} 124 | onChange={this.handleChange} 125 | placeholder={this.props.placeholder} 126 | modules={{ 127 | toolbar: { 128 | container: [ 129 | [{ header: '1' }, { header: '2' }, { header: [3, 4, 5, 6] }], 130 | ['bold', 'italic', 'underline', 'strike', 'blockquote'], 131 | [{ list: 'ordered' }, { list: 'bullet' }], 132 | 133 | ['link', 'image', 'video'], 134 | ['clean'], 135 | ['code-block'] 136 | ], 137 | handlers: { 138 | image: this.imageHandler 139 | } 140 | } 141 | }} 142 | /> 143 |
144 | 147 |
148 | 149 | 150 | ); 151 | } 152 | } 153 | 154 | export default MyComponent; -------------------------------------------------------------------------------- /client/@meowmeow/components/QuestionNew/backup.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Component } from 'react'; 2 | import { useRouter } from 'next/router'; 3 | import dynamic from 'next/dynamic'; 4 | import toast from 'react-hot-toast' 5 | import 'react-quill/dist/quill.snow.css'; 6 | import IntlMessages from '../../utils/IntlMessages' 7 | import Select from 'react-select' 8 | import Modal from '../Modal' 9 | import { Axios } from '../../modules/apiService/config' 10 | 11 | 12 | 13 | /* 14 | * Quill editor formats 15 | * See https://quilljs.com/docs/formats/ 16 | */ 17 | const formats = [ 18 | 'header', 'font', 'size', 19 | 'bold', 'italic', 'underline', 'strike', 'blockquote', 20 | 'list', 'bullet', 'indent', 21 | 'link', 'image', 'video', 'code-block' 22 | ] 23 | 24 | const tags = [ 25 | { value: 'Python', label: 'Python' }, 26 | { value: 'C++', label: 'C++' }, 27 | { value: 'Linux', label: 'Linux' } 28 | ] 29 | 30 | function saveToServer(file) { 31 | const fd = new FormData(); 32 | fd.append('photo', file); 33 | Axios 34 | .post('/question/upload', fd, { 35 | headers: { 36 | 'Content-Type': 'multipart/form-data' 37 | } 38 | }) 39 | .then(({ data }) => { 40 | if (data.statusCode == "200") { 41 | return data.data; 42 | } 43 | else { 44 | toast.error(data.message); 45 | } 46 | }) 47 | .catch(function (error) { 48 | toast.error(error.message); 49 | }) 50 | } 51 | 52 | const QuillNoSSRWrapper = dynamic(import('react-quill'), { 53 | ssr: false, 54 | loading: () => , 55 | }) 56 | 57 | class newPost extends Component { 58 | constructor(props) { 59 | super(props) 60 | this.handleSubmitModal = this.handleSubmitModal.bind(this) 61 | this.handleCloseModal = this.handleCloseModal.bind(this) 62 | this.handleEditboxChange = this.handleEditboxChange.bind(this) 63 | this.handleTagsChange = this.handleTagsChange.bind(this) 64 | this.handleSubmit = this.handleSubmit.bind(this) 65 | this.imageHandler = this.imageHandler.bind(this) 66 | this.editor = React.createRef() 67 | this.state = { detail: null, tags: null, openModal: false, modalType: null } 68 | } 69 | 70 | imageHandler = (editor) => { 71 | const input = document.createElement('input'); 72 | let quillEditor = editor 73 | // console.log(editor) 74 | input.setAttribute('type', 'file'); 75 | input.setAttribute('accept', 'image/*'); 76 | input.click(); 77 | input.onchange = async function () { 78 | const file = input.files[0]; 79 | // console.log('User trying to uplaod this:', file); 80 | 81 | const link = await saveToServer(file); 82 | quillEditor.insertEmbed(null, "image", link); 83 | }.bind(this); // react thing 84 | } 85 | 86 | handleSubmitModal = () => { 87 | 88 | } 89 | 90 | handleCloseModal = () => { 91 | this.setState({ openModal: false }) 92 | } 93 | 94 | handleEditboxChange = (content, delta, source, editor) => { 95 | this.setState({ detail: editor.getHTML() }) 96 | } 97 | 98 | handleTagsChange = (selectedOption) => { 99 | this.setState({ tags: selectedOption }) 100 | } 101 | 102 | handleSubmit = () => { 103 | this.setState({ openModal: true, modalType: 'loading' }) 104 | setTimeout(() => this.setState({ openModal: true, modalType: null }), 2000) 105 | // console.log(this.state) 106 | } 107 | 108 | render() { 109 | 110 | return ( 111 | <> 112 | 117 | {this.state.detail} 118 | 119 |

120 | 121 |

122 | 123 |

124 | 149 |

150 | setEmail(e.target.value)} 42 | /> 43 | 44 |
45 | 51 | setPassword(e.target.value)} 56 | /> 57 |
58 |
59 | 69 |
70 | 71 |
72 | 79 |
80 | 81 | 82 | 83 | 102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /server/bin/server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | var app = require('../app'); 8 | const room = require('../src/models/roomModel'); 9 | const { Server } = require('socket.io'); 10 | var http = require('http'); 11 | 12 | /** 13 | * Get port from environment and store in Express. 14 | */ 15 | 16 | var port = normalizePort(process.env.PORT || '5000'); 17 | app.set('port', port); 18 | 19 | /** 20 | * Create HTTP server. 21 | */ 22 | 23 | var server = http.createServer(app); 24 | 25 | const io = new Server(server, { 26 | cors: { 27 | origin: [process.env.clientI, process.env.clientII] 28 | } 29 | }); 30 | 31 | io.of('/live').on("connection", (socket) => { 32 | socket.emit("myID", socket.id); 33 | }) 34 | 35 | // io.engine.generateId = function (req) { 36 | // console.log(referer); 37 | // return Math.random() * 100000; 38 | // } 39 | 40 | io.of('/room').on("connection", async (socket) => { 41 | socket.emit("me", socket.id); 42 | socket.on("create room", (id) => { 43 | socket.join(id); 44 | socket.roomID = id; 45 | io.of('/live').emit("update room"); 46 | }) 47 | 48 | socket.on("join room", (id) => { 49 | socket.join(id); 50 | socket.roomID = id; 51 | socket.broadcast.to(id).emit('remote join room', socket.id); 52 | socket.broadcast.to(id).emit('new chat break', socket.id + ' joined room'); 53 | io.of('/live').emit("update room"); 54 | }); 55 | 56 | io.of("/room").adapter.on("create-room", (room) => { 57 | console.log(`room ${room} was created`); 58 | }); 59 | 60 | socket.on('turn webcam off', (roomID) => { 61 | socket.broadcast.to(roomID).emit('remote turned webcam off'); 62 | }); 63 | 64 | socket.on('turn webcam on', (roomID) => { 65 | socket.broadcast.to(roomID).emit('remote turned webcam on'); 66 | }); 67 | 68 | socket.on('start sharing screen', (roomID) => { 69 | socket.broadcast.to(roomID).emit('remote started sharing screen'); 70 | }); 71 | 72 | socket.on('stop sharing screen', (roomID) => { 73 | socket.broadcast.to(roomID).emit('remote stoped sharing screen'); 74 | }); 75 | 76 | socket.on('start sharing audio', (roomID) =>{ 77 | socket.broadcast.to(roomID).emit('remote started sharing audio'); 78 | }); 79 | 80 | socket.on('stop sharing audio', (roomID) =>{ 81 | socket.broadcast.to(roomID).emit('remote stoped sharing audio'); 82 | }); 83 | 84 | socket.on("me chat", ({content, roomID}) =>{ 85 | socket.broadcast.to(roomID).emit('remote chatted', content); 86 | }); 87 | 88 | socket.on("me send code", ({content, roomID}) =>{ 89 | socket.broadcast.to(roomID).emit('remote sent code', content); 90 | }); 91 | 92 | socket.on("new chat break", ({content, roomID}) =>{ 93 | socket.broadcast.to(roomID).emit("new chat break", content); 94 | }); 95 | 96 | //Khi 1 người ngắt kết nối với room sẽ xóa socket id của người đó lưu trong DB ra 97 | //Nếu người đó là chủ room thì sẽ xóa phòng và thông báo cuộc gọi kết thúc 98 | //Nếu người đó là người join vào thì sẽ xóa socket.id của người đó đi và giảm userCount xuống 1 đơn vị đồng thời thông báo chủ room biết có người rời đi 99 | socket.on('disconnect', () => { 100 | //Tìm room mà người vừa disconnect đó ở 101 | // room.findOneAndDelete({userCount : 0}); //khỏi đợi 102 | room.findOne({ 103 | $or: [ 104 | { userID1: socket.id }, 105 | { userID2: socket.id }, 106 | ] 107 | }) 108 | .then((_room) => { 109 | //Nếu room này đã xóa ở request khác rồi 110 | if (!_room) { 111 | return; 112 | } 113 | 114 | //Nếu còn một người hoặc là chủ room là người thoát thì delete //Lúc nào cũng là chủ room vì chủ room mà thoát thì phòng cũng phải kết thức. logic <= 1 là lâu lâu bị lỗi nó xóa luôn 115 | if (_room.userCount <= 1 || _room.userID1 == socket.id) { 116 | room.findOneAndDelete({roomID : _room.roomID}).then(() => {socket.broadcast.to(_room.roomID).emit('end call'); io.of('/live').emit("update room");}); 117 | } else { 118 | const userID2 = _room.userID2; 119 | //nếu không phải chủ room 120 | room.findOneAndUpdate({ 121 | roomID : _room.roomID 122 | }, { $inc: { userCount: -1 }, $set : {userID2 : ''} }) 123 | .then(() => { 124 | console.log(socket.id + ' leave room'); 125 | }) 126 | .catch((err) => { 127 | console.log(err); 128 | }) 129 | .finally(() =>{ 130 | //thông báo chủ room biết có người vừa thoát 131 | io.of('/live').emit("update room"); 132 | socket.broadcast.to(_room.roomID).emit('remote leave call'); 133 | socket.broadcast.to(_room.roomID).emit('new chat break', userID2 + ' left room'); 134 | }) 135 | } 136 | }) 137 | .catch((err) => { 138 | console.log(err); 139 | }); 140 | }); 141 | }) 142 | 143 | /** 144 | * Listen on provided port, on all network interfaces. 145 | */ 146 | 147 | server.listen(port); 148 | server.on('error', onError); 149 | server.on('listening', onListening); 150 | 151 | /** 152 | * Normalize a port into a number, string, or false. 153 | */ 154 | 155 | function normalizePort(val) { 156 | var port = parseInt(val, 10); 157 | 158 | if (isNaN(port)) { 159 | // named pipe 160 | return val; 161 | } 162 | 163 | if (port >= 0) { 164 | // port number 165 | return port; 166 | } 167 | 168 | return false; 169 | } 170 | 171 | /** 172 | * Event listener for HTTP server "error" event. 173 | */ 174 | 175 | function onError(error) { 176 | if (error.syscall !== 'listen') { 177 | throw error; 178 | } 179 | 180 | var bind = typeof port === 'string' 181 | ? 'Pipe ' + port 182 | : 'Port ' + port; 183 | 184 | // handle specific listen errors with friendly messages 185 | switch (error.code) { 186 | case 'EACCES': 187 | console.error(bind + ' requires elevated privileges'); 188 | process.exit(1); 189 | break; 190 | case 'EADDRINUSE': 191 | console.error(bind + ' is already in use'); 192 | process.exit(1); 193 | break; 194 | default: 195 | throw error; 196 | } 197 | } 198 | 199 | /** 200 | * Event listener for HTTP server "listening" event. 201 | */ 202 | 203 | function onListening() { 204 | var addr = server.address(); 205 | var bind = typeof addr === 'string' 206 | ? 'pipe ' + addr 207 | : 'url ' + 'http://localhost:' + addr.port; //dev 208 | console.log('Listening on ' + bind); 209 | } 210 | -------------------------------------------------------------------------------- /client/@meowmeow/components/auth/SignUp.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from "next/link"; 3 | import Auth from "../Layout/Auth"; 4 | import toast from 'react-hot-toast'; 5 | import IntlMessages from '../../utils/IntlMessages'; 6 | import { useAuth } from '../../authentication'; 7 | 8 | export default function Login() { 9 | const { userSignup } = useAuth(); 10 | const [name, setName] = useState(null); 11 | const [email, setEmail] = useState(null); 12 | const [password, setPassword] = useState(null); 13 | const [rePassword, setRePassword] = useState(null); 14 | const onsignup = () => { 15 | if (password === null || rePassword === null || name === null || email === null) 16 | toast.error(); 17 | else { 18 | if (password === rePassword) 19 | userSignup({ email, password, name }) 20 | else 21 | toast.error(); 22 | } 23 | 24 | }; 25 | 26 | return ( 27 | <> 28 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |

37 | 38 |

39 |
40 |
41 | 47 | setName(e.target.value)} 52 | /> 53 |
54 |
55 | 61 | setEmail(e.target.value)} 66 | /> 67 |
68 |
69 | 75 | setPassword(e.target.value)} 80 | /> 81 |
82 |
83 | 89 | setRePassword(e.target.value)} 94 | /> 95 |
96 |
97 | 104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /client/pages/index.js: -------------------------------------------------------------------------------- 1 | import AuthPage from '../@meowmeow/components/Layout/TrunkeyAuth'; 2 | import Link from 'next/link'; 3 | import IntlMessages from '../@meowmeow/utils/IntlMessages'; 4 | import { Heading } from '../@meowmeow/modules' 5 | 6 | const HomePage = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 |
13 |

16 | 17 |

18 |

21 | 22 |

23 | 24 | 29 | 30 | 31 | 35 | 36 | 37 | 38 |
39 | 40 |
41 |
44 |
45 |
46 | 47 |
48 |
51 |

54 | 55 |

56 |
57 |
60 |
61 | 62 |
65 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 95 | 98 | 99 | 100 |
101 |
102 |
103 | 104 |
105 |

108 | 109 |

110 |
111 |
114 |
115 | 116 |

117 | 118 |

119 | 120 | 124 | 125 | 126 |
127 |
128 |
129 | ) 130 | }; 131 | 132 | export default HomePage; -------------------------------------------------------------------------------- /client/@meowmeow/components/Header/fullPage/index.js: -------------------------------------------------------------------------------- 1 | import IntlMessages from '../../../utils/IntlMessages'; 2 | import { MenuSecond, MenuPublic } from '../Menu'; 3 | import LanguageSwitcher from '../../LanguageSwitcher'; 4 | import ProfileGroup from '../../ProfileGroup'; 5 | import { loggedIn } from '../../../authentication'; 6 | import Link from 'next/link'; 7 | import { useRouter } from 'next/router'; 8 | 9 | export default function Header({ children }) { 10 | const authUser = loggedIn(); 11 | const router = useRouter(); 12 | return ( 13 |
14 | 15 |
16 | {/* Navbar */} 17 |
18 |
19 | 22 |
23 |
24 | 25 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 | {authUser ? : <> 39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | } 51 |
52 |
53 | {/* Page content here */} 54 |
55 | {children} 56 |
57 |
58 |
59 |
72 |
73 | ); 74 | } 75 | 76 | // import IntlMessages from '../../../utils/IntlMessages'; 77 | // import { MenuSecond, MenuPublic } from '../Menu'; 78 | // import LanguageSwitcher from '../../LanguageSwitcher'; 79 | // import ProfileGroup from '../../ProfileGroup'; 80 | // import { loggedIn } from '../../../authentication'; 81 | // import Link from 'next/link'; 82 | // import { useRouter } from 'next/router'; 83 | 84 | // export default function Header({ children }) { 85 | // const authUser = loggedIn(); 86 | // const router = useRouter(); 87 | // return ( 88 | //
89 | // 90 | //
91 | // {/* Navbar */} 92 | //
93 | //
94 | // 97 | //
98 | //
99 | // 100 | // 101 | // 102 | //
103 | // 104 | //
105 | //
106 | //
107 | // 108 | //
109 | //
110 | // {authUser ? : <> 111 | // 112 | // 117 | // 118 | // 119 | // 124 | // 125 | // 126 | // } 127 | //
128 | //
129 | // {/* Page content here */} 130 | //
131 | // {children} 132 | //
133 | //
134 | //
135 | //
148 | //
149 | // ); 150 | // } --------------------------------------------------------------------------------