├── 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 |
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 |
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 |
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 |
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 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/client/components/HostLeft.jsx:
--------------------------------------------------------------------------------
1 | export default function HostLeft() {
2 | return (
3 |
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 |
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 |
24 |
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 |
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 |
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 |
10 |
16 |
32 |
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 |
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 |
23 |
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 |
60 |
61 |
64 |
65 |
70 |
71 |
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 |
21 |
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 |
67 |
68 |
71 |
72 |
73 |
74 |
79 |
80 |
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 |

91 |
92 |
93 |
94 |
{user.name}
95 |
96 |
97 |
{date2local(time)}
98 |
99 |
100 |
101 |
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 |