├── .gitignore ├── README.md ├── client ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src │ ├── AllRoutes.jsx │ ├── App.css │ ├── App.js │ ├── actions │ ├── auth.js │ ├── currentUser.js │ ├── question.js │ └── users.js │ ├── api │ └── index.js │ ├── assets │ ├── Globe.svg │ ├── bars-solid.svg │ ├── blacklogo.svg │ ├── comment-alt-solid.svg │ ├── icon.png │ ├── logo.png │ ├── pen-solid.svg │ ├── search-solid.svg │ ├── sort-down.svg │ └── sort-up.svg │ ├── components │ ├── Avatar │ │ └── Avatar.jsx │ ├── Chatbot │ │ ├── ChatMessage.jsx │ │ ├── Chatbot.css │ │ ├── Chatbot.jsx │ │ └── Form.jsx │ ├── Editor │ │ └── Editor.jsx │ ├── HomeMainbar │ │ ├── HomeMainbar.css │ │ ├── HomeMainbar.jsx │ │ ├── QuestionList.jsx │ │ └── Questions.jsx │ ├── LeftSidebar │ │ ├── LeftSidebar.css │ │ └── LeftSidebar.jsx │ ├── Loader │ │ └── Loader.jsx │ ├── Navbar │ │ ├── Navbar.css │ │ └── Navbar.jsx │ └── RightSidebar │ │ ├── RightSidebar.css │ │ ├── RightSidebar.jsx │ │ ├── Widget.jsx │ │ └── WidgetTags.jsx │ ├── index.css │ ├── index.js │ ├── pages │ ├── AskQuestion │ │ ├── AskQuestion.css │ │ └── AskQuestion.jsx │ ├── Auth │ │ ├── AboutAuth.jsx │ │ ├── Auth.css │ │ └── Auth.jsx │ ├── Home │ │ └── Home.jsx │ ├── Questions │ │ ├── DisplayAnswer.jsx │ │ ├── DisplayQuestion.jsx │ │ ├── Questions.css │ │ ├── Questions.jsx │ │ └── QuestionsDetails.jsx │ ├── Tags │ │ ├── Tags.css │ │ ├── Tags.jsx │ │ ├── TagsList.jsx │ │ └── tagList.js │ ├── UserProfile │ │ ├── EditProfileForm.jsx │ │ ├── ProfileBio.jsx │ │ ├── UserProfile.css │ │ └── UserProfile.jsx │ └── Users │ │ ├── User.jsx │ │ ├── Users.css │ │ └── Users.jsx │ └── reducers │ ├── auth.js │ ├── currentUser.js │ ├── index.js │ ├── questions.js │ └── users.js └── server ├── .gitignore ├── config └── connectDB.js ├── controllers ├── answer.js ├── auth.js ├── chatbot.js ├── otp.js ├── questions.js └── users.js ├── index.js ├── middleware └── auth.js ├── models ├── auth.js ├── otp.js └── questions.js ├── package-lock.json ├── package.json ├── routes ├── Answers.js ├── Chatbot.js ├── Otp.js ├── Questions.js └── Users.js └── utils ├── generateOTP.js ├── hashData.js └── sendEmail.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | /client/.env -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stackoverflow-Clone-MERN 2 | 3 | ## Updates: 4 | - **Integrated AI chatbot using OpenAI** 5 | - ***Chatbot won't give response as the free trial got expired on 1 June 2023*** 6 | 7 | 8 | 9 | ### How to run the application locally: 10 | 11 | 1. Clone the project. 12 | 13 | 1. Install dependencies in both **client** and **server** folder by using
command `npm install` or `npm i` 14 | 15 | 1. Complete the .env file in **server** folder. 16 | - Connect to MongoDB. 17 | - Enter remaining fields. 18 | 19 | 1. To run the application, 20 | - move to **server** folder using command `cd server`
Then use command `npm start` to start the server. 21 | - move to **client** folder using command `cd client`
Then use command `npm start` to start the application. 22 | 23 | ### Give it a star if helpful! 24 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env 17 | .env.local 18 | .env.development.local 19 | .env.test.local 20 | .env.production.local 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "proxy": "https://stack-overflow-clone-server-2bw6.onrender.com", 5 | "private": true, 6 | "dependencies": { 7 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 8 | "@fortawesome/free-solid-svg-icons": "^6.4.0", 9 | "@fortawesome/react-fontawesome": "^0.2.0", 10 | "@testing-library/jest-dom": "^5.16.5", 11 | "@testing-library/react": "^13.4.0", 12 | "@testing-library/user-event": "^13.5.0", 13 | "axios": "^1.3.4", 14 | "html-react-parser": "^3.0.15", 15 | "jwt-decode": "^3.1.2", 16 | "moment": "^2.29.4", 17 | "nanoid": "^4.0.2", 18 | "react": "^18.2.0", 19 | "react-copy-to-clipboard": "^5.1.0", 20 | "react-dom": "^18.2.0", 21 | "react-hot-toast": "^2.4.0", 22 | "react-icons": "^4.8.0", 23 | "react-loader-spinner": "^5.3.4", 24 | "react-quill": "^2.0.0", 25 | "react-redux": "^8.0.5", 26 | "react-router-dom": "^6.9.0", 27 | "react-scripts": "5.0.1", 28 | "redux": "^4.2.1", 29 | "redux-thunk": "^2.4.2", 30 | "web-vitals": "^2.1.4" 31 | }, 32 | "scripts": { 33 | "start": "react-scripts start", 34 | "build": "react-scripts build", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "eslintConfig": { 39 | "extends": [ 40 | "react-app", 41 | "react-app/jest" 42 | ] 43 | }, 44 | "browserslist": { 45 | "production": [ 46 | ">0.2%", 47 | "not dead", 48 | "not op_mini all" 49 | ], 50 | "development": [ 51 | "last 1 chrome version", 52 | "last 1 firefox version", 53 | "last 1 safari version" 54 | ] 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gjha133/stackoverflow-clone-mern/e4f810ff1eb200afa91ebd8fade64b497d6880e4/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Stack Overflow Clone 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/src/AllRoutes.jsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom' 2 | import Auth from './pages/Auth/Auth' 3 | import Home from './pages/Home/Home' 4 | import Questions from './pages/Questions/Questions' 5 | import AskQuestion from './pages/AskQuestion/AskQuestion' 6 | import DisplayQuestion from './pages/Questions/DisplayQuestion' 7 | import Tags from './pages/Tags/Tags' 8 | import Users from "./pages/Users/Users" 9 | import UserProfile from "./pages/UserProfile/UserProfile" 10 | 11 | const AllRoutes = () => { 12 | return ( 13 | 14 | } /> 15 | } /> 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | } /> 21 | } /> 22 | 23 | ) 24 | } 25 | 26 | export default AllRoutes -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | .home-container-1 { 2 | min-height: 100vh; 3 | max-width: 1250px; 4 | width: 100%; 5 | display: flex; 6 | justify-content: space-between; 7 | margin: 0% auto; 8 | } 9 | 10 | .home-container-2 { 11 | max-width: 1100px; 12 | width: calc(100% - 164px); 13 | padding: 24px; 14 | box-sizing: border-box; 15 | } 16 | 17 | @media screen and (max-width: 760px) { 18 | .home-container-2 { 19 | max-width: 100%; 20 | width: 100%; 21 | } 22 | } 23 | 24 | .open-chatbot { 25 | position: sticky; 26 | bottom: 5%; 27 | left: 85%; 28 | background-color: #ef8236; 29 | border: none; 30 | padding: 20px; 31 | border-radius: 10px; 32 | cursor: pointer; 33 | font-size: 1rem; 34 | } 35 | 36 | .open-chatbot:hover { 37 | background-color: #ef83369a; 38 | } 39 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router } from "react-router-dom"; 2 | import { useEffect, useState } from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import "./App.css"; 5 | import Navbar from "./components/Navbar/Navbar"; 6 | import AllRoutes from "./AllRoutes"; 7 | import { fetchAllQuestions } from "./actions/question"; 8 | import { fetchAllUsers } from "./actions/users"; 9 | import { Toaster } from 'react-hot-toast'; 10 | import Chatbot from "./components/Chatbot/Chatbot"; 11 | 12 | function App() { 13 | const [isOpen, setIsOpen] = useState(false) 14 | const [isVerified, setIsVerified] = useState(false) 15 | const dispatch = useDispatch() 16 | 17 | useEffect(() => { 18 | dispatch(fetchAllQuestions()) 19 | dispatch(fetchAllUsers()); 20 | }, [dispatch]) 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | {isOpen ? ( 29 | 34 | ) 35 | : } 41 | 42 |
43 | ); 44 | } 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /client/src/actions/auth.js: -------------------------------------------------------------------------------- 1 | import * as api from "../api"; 2 | import { setCurrentUser } from "./currentUser"; 3 | 4 | export const signup = (authData, navigate) => async (dispatch) => { 5 | try { 6 | const { data } = await api.signUp(authData) 7 | dispatch({ type: 'AUTH', data }) 8 | dispatch(setCurrentUser(JSON.parse(localStorage.getItem("Profile")))) 9 | navigate('/') 10 | } catch (error) { 11 | console.log(error) 12 | } 13 | } 14 | 15 | export const login = (authData, navigate) => async (dispatch) => { 16 | try { 17 | const { data } = await api.logIn(authData) 18 | dispatch({ type: 'AUTH', data }) 19 | dispatch(setCurrentUser(JSON.parse(localStorage.getItem("Profile")))) 20 | navigate('/') 21 | } catch (error) { 22 | console.log(error) 23 | } 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /client/src/actions/currentUser.js: -------------------------------------------------------------------------------- 1 | export const setCurrentUser = (data) => { 2 | return { 3 | type: "FETCH_CURRENT_USER", 4 | payload: data, 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /client/src/actions/question.js: -------------------------------------------------------------------------------- 1 | import * as api from '../api' 2 | 3 | export const askQuestion = (questionData, navigate) => async (dispatch) => { 4 | try { 5 | const { data } = await api.postQuestion(questionData) 6 | dispatch({ type: 'POST_QUESTION', payload: data}) 7 | dispatch(fetchAllQuestions()) 8 | navigate('/') 9 | } catch (error) { 10 | console.log(error) 11 | } 12 | } 13 | 14 | export const fetchAllQuestions = () => async (dispatch) => { 15 | try { 16 | const { data } = await api.getAllQuestions() 17 | dispatch({ type: 'FETCH_ALL_QUESTIONS', payload: data }) 18 | } catch (error) { 19 | console.log(error) 20 | } 21 | } 22 | 23 | export const postAnswer = (answerData) => async (dispatch) => { 24 | try { 25 | const { id, noOfAnswers, answerBody,userAnswered, userId } = answerData; 26 | const { data } = await api.postAnswer( 27 | id, 28 | noOfAnswers, 29 | answerBody, 30 | userAnswered, 31 | userId 32 | ); 33 | dispatch({ type: "POST_ANSWER", payload: data }); 34 | dispatch(fetchAllQuestions()); 35 | } catch (error) { 36 | console.log(error); 37 | } 38 | }; 39 | 40 | export const deleteQuestion = (id, navigate) => async (dispatch) => { 41 | try { 42 | await api.deleteQuestion(id); 43 | dispatch(fetchAllQuestions()); 44 | navigate("/"); 45 | } catch (error) { 46 | console.log(error); 47 | } 48 | }; 49 | 50 | export const voteQuestion = (id, value, userId) => async (dispatch) => { 51 | try { 52 | await api.voteQuestion(id, value, userId) 53 | dispatch(fetchAllQuestions()); 54 | } catch (error) { 55 | console.log(error) 56 | } 57 | } 58 | 59 | export const deleteAnswer = (id, answerId, noOfAnswers) => async (dispatch) => { 60 | try { 61 | await api.deleteAnswer(id, answerId, noOfAnswers); 62 | dispatch(fetchAllQuestions()); 63 | } catch (error) { 64 | console.log(error); 65 | } 66 | }; 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /client/src/actions/users.js: -------------------------------------------------------------------------------- 1 | import * as api from '../api' 2 | 3 | export const fetchAllUsers = () => async (dispatch) => { 4 | try { 5 | const { data } = await api.fetchAllUsers(); 6 | dispatch({ type: "FETCH_USERS", payload: data }); 7 | } catch (error) { 8 | console.log(error); 9 | } 10 | }; 11 | 12 | export const updateProfile = (id, updatedData) => async (dispatch) => { 13 | try { 14 | const { data } = await api.updateProfile(id, updatedData) 15 | dispatch({type: 'UPDATE_CURRENT_USER', payload: data}) 16 | } catch (error) { 17 | console.log(error) 18 | } 19 | } -------------------------------------------------------------------------------- /client/src/api/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | const API = axios.create({ baseURL: "https://stack-overflow-clone-server-vq27.onrender.com"}) 4 | 5 | API.interceptors.request.use((req) => { 6 | if (localStorage.getItem("Profile")) { 7 | req.headers.authorization = `Bearer ${ 8 | JSON.parse(localStorage.getItem("Profile")).token 9 | }`; 10 | } 11 | return req; 12 | }); 13 | 14 | // AUTH 15 | export const logIn = (authData) => ( 16 | API.post('/user/login', authData) 17 | ) 18 | export const signUp = (authData) => ( 19 | API.post('/user/signup', authData) 20 | ) 21 | 22 | // QUESTION 23 | export const postQuestion = (questionData) => ( 24 | API.post('/questions/Ask', questionData) 25 | ) 26 | export const getAllQuestions = () => ( 27 | API.get('/questions/All') 28 | ) 29 | export const deleteQuestion = (id) => ( 30 | API.delete(`/questions/delete/${id}`) 31 | ) 32 | export const voteQuestion = (id, value, userId) => ( 33 | API.patch(`/questions/vote/${id}`, { value, userId }) 34 | ) 35 | 36 | // ANSWER 37 | export const postAnswer = (id, noOfAnswers, answerBody, userAnswered, userId) => ( 38 | API.patch(`/answer/post/${id}`, {id, noOfAnswers, answerBody, userAnswered, userId}) 39 | ) 40 | export const deleteAnswer = (id, answerId, noOfAnswers) => 41 | API.patch(`/answer/delete/${id}`, { answerId, noOfAnswers }); 42 | 43 | // USER 44 | export const fetchAllUsers = () => API.get("/user/getAllUsers"); 45 | 46 | export const updateProfile = (id, updatedData) => 47 | API.patch(`/user/update/${id}`, updatedData); 48 | -------------------------------------------------------------------------------- /client/src/assets/Globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/bars-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/blacklogo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/comment-alt-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gjha133/stackoverflow-clone-mern/e4f810ff1eb200afa91ebd8fade64b497d6880e4/client/src/assets/icon.png -------------------------------------------------------------------------------- /client/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gjha133/stackoverflow-clone-mern/e4f810ff1eb200afa91ebd8fade64b497d6880e4/client/src/assets/logo.png -------------------------------------------------------------------------------- /client/src/assets/pen-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/search-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/sort-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/sort-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/components/Avatar/Avatar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Avatar = ({ 4 | children, 5 | backgroundColor, 6 | px, 7 | py, 8 | color, 9 | borderRadius, 10 | fontSize, 11 | cursor, 12 | }) => { 13 | const style = { 14 | backgroundColor, 15 | padding: `${py} ${px}`, 16 | color: color || "black", 17 | borderRadius, 18 | fontSize, 19 | textAlign: "center", 20 | cursor: cursor || null, 21 | textDecoration: "none", 22 | }; 23 | 24 | return
{children}
; 25 | }; 26 | 27 | export default Avatar; -------------------------------------------------------------------------------- /client/src/components/Chatbot/ChatMessage.jsx: -------------------------------------------------------------------------------- 1 | 2 | const ChatMessage = ({ message }) => { 3 | return ( 4 |
5 |
6 | { 7 | message.user === 'gpt' ? ` 8 | AI : ${message.message} 9 | `: ` 10 | User : ${message.message} 11 | ` 12 | } 13 |
14 |
15 | ) 16 | } 17 | 18 | export default ChatMessage -------------------------------------------------------------------------------- /client/src/components/Chatbot/Chatbot.css: -------------------------------------------------------------------------------- 1 | .chatbot-container { 2 | position: sticky; 3 | left: 85%; 4 | bottom: 0; 5 | width: 22.5rem; 6 | height: 30rem; 7 | padding: 0px; 8 | margin: 0; 9 | color: black; 10 | transition: all 0.3s; 11 | } 12 | 13 | .chatbot-header { 14 | display: flex; 15 | align-items: center; 16 | justify-content: space-between; 17 | padding: 0 10px; 18 | margin: 2px; 19 | height: 10%; 20 | background-color: #fbf3d5; 21 | box-shadow: 0px 4px #ef8236; 22 | } 23 | 24 | .chatbot-header1 { 25 | display: flex; 26 | } 27 | 28 | .chatbot-header p { 29 | font-weight: bold; 30 | } 31 | 32 | .chatbot-header p:hover { 33 | color: #ef8236; 34 | } 35 | 36 | .chatbot-icon { 37 | cursor: pointer; 38 | padding: 5px; 39 | margin: 5px; 40 | margin-right: 10px; 41 | border-radius: 30%; 42 | } 43 | 44 | .chatbot-icon:hover { 45 | background-color: #ef8236; 46 | color: white; 47 | } 48 | 49 | .chatbot-chatwindow { 50 | height: 80%; 51 | margin: 2px; 52 | background-color: #fbf3d5; 53 | box-shadow: 0px 4px #ef8236; 54 | overflow-x: hidden; 55 | overflow-y: auto; 56 | position: relative; 57 | } 58 | 59 | .chatbot-chatwindow-starter { 60 | margin-left: 10px; 61 | } 62 | 63 | .chat-message { 64 | background-color: #f1e5bc; 65 | padding: 5px 0; 66 | } 67 | 68 | .chatgpt { 69 | background-color: #ef8236; 70 | } 71 | 72 | .message { 73 | padding: 2px 10px; 74 | } 75 | 76 | .chatbot-form { 77 | background-color: #fbf3d5; 78 | display: flex; 79 | align-items: center; 80 | margin: 1px; 81 | } 82 | 83 | .chatbot-form input { 84 | width: 85%; 85 | height: 30px; 86 | border: none; 87 | padding: 5px; 88 | } 89 | 90 | .chatbot-form input:focus { 91 | outline: none; 92 | } 93 | .chatbot-form input::placeholder { 94 | color: black; 95 | } 96 | 97 | .chatbot-button { 98 | border: none; 99 | background-color: #ef8236; 100 | cursor: pointer; 101 | border-radius: 30%; 102 | padding: 5px 10px; 103 | } 104 | 105 | .chatbot-button:hover { 106 | background-color: #ef83369a; 107 | } 108 | 109 | .chatbot-chatwindow-form { 110 | position: absolute; 111 | left: 20%; 112 | top: 20%; 113 | display: flex; 114 | flex-direction: column; 115 | align-items: center; 116 | justify-content: center; 117 | } 118 | 119 | .chatbot-chatwindow-form div { 120 | padding: 10px; 121 | text-align: center; 122 | } 123 | 124 | .chatbot-chatwindow-form p { 125 | font-weight: bold; 126 | } 127 | 128 | .chatbot-chatwindow-form input { 129 | height: 2rem; 130 | width: 12rem; 131 | border-radius: 2px; 132 | border: none; 133 | background-color: #f1e5bc; 134 | box-shadow: 0px 1px #ef8236; 135 | } 136 | 137 | .chatbot-chatwindow-form button { 138 | background-color: #ef8236; 139 | border: none; 140 | padding: 10px; 141 | border-radius: 10px; 142 | cursor: pointer; 143 | } 144 | 145 | .chatbot-chatwindow-form button:hover { 146 | background-color: #ef83369a; 147 | } 148 | -------------------------------------------------------------------------------- /client/src/components/Chatbot/Chatbot.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { BsArrowBarDown } from 'react-icons/bs' 3 | import { AiOutlineSend, AiOutlineDelete } from 'react-icons/ai' 4 | import './Chatbot.css' 5 | import ChatMessage from './ChatMessage' 6 | import Form from './Form' 7 | import toast from 'react-hot-toast' 8 | 9 | const Chatbot = ({ setIsOpen, isVerified, setIsVerified }) => { 10 | const [email, setEmail] = useState('') 11 | const [otp, setOtp] = useState('') 12 | const [sentEmail, setSentEmail] = useState(false) 13 | const [input, setInput] = useState('') 14 | const [chatLog, setChatLog] = useState([]) 15 | 16 | const handleSubmit = async (e) => { 17 | e.preventDefault() 18 | const chatLogNew = [...chatLog, { user: 'me', message: `${input}` }] 19 | setInput('') 20 | setChatLog(chatLogNew) 21 | const messages = chatLogNew.map(message => message.message).join('\n') 22 | try { 23 | const response = await fetch('https://stack-overflow-clone-server-vq27.onrender.com/chatbot/', { 24 | method: 'POST', 25 | headers: { 26 | 'Content-Type': 'application/json' 27 | }, 28 | body: JSON.stringify({ 29 | message: messages 30 | }) 31 | }) 32 | const data = await response.json() 33 | setChatLog([...chatLogNew, { user: 'gpt', message: `${data.message}` }]) 34 | } catch (error) { 35 | setChatLog([...chatLogNew, { user: 'gpt', message: 'Some error occurred. Try another question' }]) 36 | } 37 | } 38 | 39 | const handleEmailSubmit = async (e) => { 40 | e.preventDefault() 41 | const response = await fetch('https://stack-overflow-clone-server-vq27.onrender.com/otp/sendOTP', { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json' 45 | }, 46 | body: JSON.stringify({ 47 | email: email, 48 | subject: 'Email Verification OTP', 49 | message: "Hi, As you are trying to verify, here is the OTP that you need to enter to verify your email address. If you didn't make this request, please ignore this email.", 50 | duration: 1 51 | }) 52 | }) 53 | const data = await response.json() 54 | if (data.otp) { 55 | setSentEmail(true) 56 | toast.success('OTP Sent Successfully. Please wait upto 1-2 mins') 57 | } else { 58 | toast.error('Try again') 59 | } 60 | } 61 | 62 | const handleOTPSubmit = async (e) => { 63 | e.preventDefault() 64 | const response = await fetch('https://stack-overflow-clone-server-vq27.onrender.com/otp/verifyOTP', { 65 | method: 'POST', 66 | headers: { 67 | 'Content-Type': 'application/json' 68 | }, 69 | body: JSON.stringify({ 70 | email: email, 71 | otp: otp 72 | }) 73 | }) 74 | const data = await response.json() 75 | if (data.valid) { 76 | toast.success('Successfully verified') 77 | setIsVerified(true) 78 | } 79 | else { 80 | toast.error('Wrong OTP!. Try again') 81 | setIsVerified(false) 82 | } 83 | setSentEmail(false) 84 | } 85 | 86 | return ( 87 |
88 |
89 |
90 | setIsOpen((isOpen) => !isOpen)} /> 94 | {isVerified && 95 | setChatLog([])} 99 | />} 100 |
101 |

AI Chatbot

102 |
103 |
104 | { 105 | !isVerified ? <> 106 | { 107 | !sentEmail ? 108 |
110 | 115 | 118 |
: 119 |
121 | 126 | 129 |
130 | } 131 | : chatLog.length === 0 && Ask your queries! 132 | } 133 | { 134 | chatLog.map((message, index) => ( 135 | 136 | )) 137 | } 138 |
139 |
140 | setInput(() => e.target.value)} className='chatbot-form' placeholder='Enter your doubt...' 144 | disabled={isVerified ? "" : "disabled"} 145 | /> 146 | 153 |
154 |
155 | ) 156 | } 157 | 158 | export default Chatbot 159 | -------------------------------------------------------------------------------- /client/src/components/Chatbot/Form.jsx: -------------------------------------------------------------------------------- 1 | import './Chatbot.css' 2 | 3 | const Form = ({ type, value, setter }) => { 4 | return ( 5 |
6 | {type === 'Email' &&

Verify email to use chatbot

} 7 | {type === 'OTP' &&

Enter OTP to verify. (1 try left)

} 8 |

{type}

9 | setter(e.target.value)} 13 | placeholder={type === 'Email' ? 'Enter email...': 'Enter OTP...'} 14 | /> 15 |
16 | ) 17 | } 18 | 19 | export default Form -------------------------------------------------------------------------------- /client/src/components/Editor/Editor.jsx: -------------------------------------------------------------------------------- 1 | import ReactQuill from 'react-quill'; 2 | import 'react-quill/dist/quill.snow.css'; 3 | 4 | export default function Editor({ value, onChange }) { 5 | const modules = { 6 | toolbar: [ 7 | ['bold', 'italic', 'underline', 'strike'], // toggled buttons 8 | ['blockquote', 'code-block'], 9 | 10 | [{ 'header': 1 }, { 'header': 2 }], // custom button values 11 | [{ 'list': 'ordered'}, { 'list': 'bullet' }], 12 | [{ 'script': 'sub'}, { 'script': 'super' }], // superscript/subscript 13 | [{ 'indent': '-1'}, { 'indent': '+1' }], // outdent/indent 14 | [{ 'direction': 'rtl' }], // text direction 15 | 16 | [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown 17 | [{ 'header': [1, 2, 3, 4, 5, 6, false] }], 18 | 19 | [{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme 20 | [{ 'font': [] }], 21 | [{ 'align': [] }], 22 | 23 | ['clean'] // remove formatting button 24 | ], 25 | }; 26 | 27 | return ( 28 | 33 | ); 34 | } -------------------------------------------------------------------------------- /client/src/components/HomeMainbar/HomeMainbar.css: -------------------------------------------------------------------------------- 1 | .main-bar { 2 | width: calc(100% - 300px - 24px); 3 | float: left; 4 | margin: 25px 0px; 5 | padding: 0; 6 | } 7 | 8 | .main-bar-header { 9 | display: flex; 10 | justify-content: space-between; 11 | align-items: center; 12 | } 13 | 14 | .main-bar-header h1 { 15 | font-weight: 400; 16 | } 17 | 18 | .main-bar-header .ask-btn { 19 | padding: 10px 15px; 20 | border-radius: 4px; 21 | background-color: #009dff; 22 | color: white; 23 | border: none; 24 | text-decoration: none; 25 | transition: 0.3s; 26 | cursor: pointer; 27 | } 28 | 29 | .main-bar-header .ask-btn:hover { 30 | background-color: #0086d8; 31 | } 32 | 33 | .display-question-container { 34 | min-height: 80px; 35 | width: 100%; 36 | display: flex; 37 | align-items: center; 38 | background-color: #fdf7e2; 39 | border-bottom: solid 1px #edeff0; 40 | } 41 | 42 | .display-question-container .display-votes-ans { 43 | padding: 10px; 44 | } 45 | 46 | .display-question-container .display-votes-ans p { 47 | margin: 0%; 48 | text-align: center; 49 | } 50 | 51 | .display-question-details { 52 | flex-grow: 1; 53 | padding: 10px; 54 | margin: 0%; 55 | } 56 | 57 | .question-title-link { 58 | text-decoration: none; 59 | color: #037ecb; 60 | transition: 0.3s; 61 | } 62 | 63 | .question-title-link:hover { 64 | color: #009dff; 65 | } 66 | 67 | .display-tags-time .display-tags { 68 | display: flex; 69 | flex-wrap: wrap; 70 | } 71 | 72 | .display-tags-time { 73 | display: flex; 74 | flex-wrap: wrap; 75 | align-items: center; 76 | justify-content: space-between; 77 | } 78 | 79 | .display-tags-time .display-tags p { 80 | margin: 2px; 81 | padding: 4px; 82 | font-size: 13px; 83 | background-color: #edeff0; 84 | color: #39739d; 85 | } 86 | .display-tags-time .display-time { 87 | font-size: 13px; 88 | } 89 | 90 | @media screen and (max-width: 1020px) { 91 | .main-bar { 92 | width: 100%; 93 | } 94 | } 95 | 96 | @media screen and (max-width: 740px) { 97 | .display-question-container .display-votes-ans { 98 | padding: 10px; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /client/src/components/HomeMainbar/HomeMainbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useLocation, useNavigate } from "react-router-dom"; 3 | import "./HomeMainbar.css"; 4 | import QuestionList from "./QuestionList"; 5 | import { useSelector } from "react-redux"; 6 | import Loader from "../Loader/Loader"; 7 | 8 | 9 | const HomeMainbar = () => { 10 | const location = useLocation(); 11 | const user = 1; 12 | const navigate = useNavigate(); 13 | 14 | const questionsList = useSelector(state => state.questionsReducer) 15 | 16 | const checkAuth = () => { 17 | if (user === null) { 18 | alert("Login or Signup to ask a question"); 19 | navigate("/Auth"); 20 | } else { 21 | navigate("/AskQuestion"); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 |
28 | {location.pathname === "/" ? ( 29 |

Top Questions

30 | ) : ( 31 |

All Questions

32 | )} 33 | 36 |
37 |
38 | {questionsList.data === null ? ( 39 | 40 | ) : ( 41 | <> 42 |

{questionsList.data.length} questions

43 | 44 | 45 | )} 46 |
47 |
48 | ); 49 | }; 50 | 51 | export default HomeMainbar; 52 | -------------------------------------------------------------------------------- /client/src/components/HomeMainbar/QuestionList.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Questions from "./Questions"; 3 | const QuestionList = ({ questionsList }) => { 4 | return ( 5 | <> 6 | {questionsList.map((question) => ( 7 | 8 | ))} 9 | 10 | ); 11 | }; 12 | 13 | export default QuestionList; 14 | -------------------------------------------------------------------------------- /client/src/components/HomeMainbar/Questions.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import moment from "moment"; 4 | 5 | 6 | const Questions = ({ question }) => { 7 | return ( 8 |
9 |
10 |

{question.upVote.length - question.downVote.length}

11 |

votes

12 |
13 |
14 |

{question.noOfAnswers}

15 |

answers

16 |
17 |
18 | 19 | {question.questionTitle.length > (window.innerWidth <= 400 ? 70 : 90) 20 | ? question.questionTitle.substring( 21 | 0, 22 | window.innerWidth <= 400 ? 70 : 90 23 | ) + "..." 24 | : question.questionTitle} 25 | 26 |
27 |
28 | {question.questionTags.map((tag) => ( 29 |

{tag}

30 | ))} 31 |
32 |

33 | asked {moment(question.askedOn).fromNow()} by {question.userPosted} 34 |

35 |
36 |
37 |
38 | ); 39 | }; 40 | 41 | export default Questions; 42 | -------------------------------------------------------------------------------- /client/src/components/LeftSidebar/LeftSidebar.css: -------------------------------------------------------------------------------- 1 | .left-sidebar { 2 | width: 164px; 3 | box-shadow: 1px 1px 0 rgba(0, 0, 0, 0.2); 4 | background-color: white; 5 | height: auto; 6 | min-height: 100vh; 7 | transition: box-shadow ease-in-out 0.1s, transform ease-in-out 0.1s; 8 | box-sizing: border-box; 9 | font-size: 13px; 10 | } 11 | 12 | .left-sidebar .nav-btn { 13 | background-color: inherit; 14 | width: 100%; 15 | border: none; 16 | padding: 0%; 17 | } 18 | 19 | .side-nav { 20 | height: auto; 21 | max-width: 100%; 22 | position: sticky; 23 | margin: 50px 0px; 24 | padding: 20px 0px; 25 | } 26 | 27 | .side-nav-div { 28 | padding: 10px 0px; 29 | } 30 | 31 | .side-nav-div div { 32 | padding-left: 10px; 33 | } 34 | 35 | .side-nav-links { 36 | text-decoration: none; 37 | color: #3a3a3a; 38 | display: flex; 39 | align-items: center; 40 | justify-content: flex-start; 41 | padding-left: 10px; 42 | transition: 0.2s; 43 | } 44 | 45 | .side-nav-links:hover { 46 | color: black; 47 | } 48 | 49 | .active { 50 | font-weight: bolder; 51 | color: black; 52 | background-color: rgb(225, 225, 225); 53 | border-right: solid 3px #ef8236; 54 | } 55 | 56 | @media screen and (max-width: 760px) { 57 | .left-sidebar { 58 | position: absolute; 59 | transform: translateX(-100%); 60 | z-index: 10; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /client/src/components/LeftSidebar/LeftSidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./LeftSidebar.css"; 3 | import { NavLink } from "react-router-dom"; 4 | import Globe from "../../assets/Globe.svg"; 5 | 6 | const LeftSidebar = () => { 7 | 8 | return ( 9 |
10 | 52 |
53 | ); 54 | }; 55 | 56 | export default LeftSidebar; 57 | -------------------------------------------------------------------------------- /client/src/components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FallingLines } from 'react-loader-spinner' 3 | 4 | const Loader = () => { 5 | return ( 6 |
12 | 18 |
19 | ) 20 | } 21 | 22 | export default Loader -------------------------------------------------------------------------------- /client/src/components/Navbar/Navbar.css: -------------------------------------------------------------------------------- 1 | .main-nav { 2 | min-height: 50px; 3 | width: 100%; 4 | margin: 0% auto; 5 | border-top: solid 3px #ef8236; 6 | box-shadow: 0px 1px 5px #00000033; 7 | position: fixed; 8 | z-index: 15; 9 | top: 0%; 10 | left: 0%; 11 | background-color: #f8f9f9; 12 | display: flex; 13 | justify-content: center; 14 | align-items: center; 15 | } 16 | 17 | .navbar { 18 | height: 100%; 19 | min-width: 85%; 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .slide-in-icon { 26 | display: none; 27 | transition: 0.2s; 28 | padding: 5px 6px 3px 6px; 29 | border-radius: 50%; 30 | background-color: inherit; 31 | border: none; 32 | cursor: pointer; 33 | } 34 | 35 | .navbar .navbar-1 { 36 | display: flex; 37 | align-items: center; 38 | flex-grow: 1; 39 | width: 40%; 40 | } 41 | 42 | .navbar .navbar-2 { 43 | display: flex; 44 | align-items: center; 45 | margin: 0 15px; 46 | width: 60%; 47 | } 48 | 49 | .nav-logo { 50 | padding: 5px 25px; 51 | } 52 | 53 | .nav-item { 54 | font-size: small; 55 | font-weight: 500; 56 | text-decoration: none; 57 | color: rgb(69, 69, 69); 58 | transition: 0.2s; 59 | } 60 | 61 | .nav-btn { 62 | cursor: pointer; 63 | border-radius: 20px; 64 | padding: 10px 20px; 65 | } 66 | 67 | .nav-item:hover, 68 | .slide-in-icon:hover { 69 | background-color: rgb(226, 226, 226); 70 | } 71 | 72 | .navbar .navbar-2 form { 73 | flex-grow: 1; 74 | padding: 0px; 75 | position: relative; 76 | } 77 | 78 | .navbar .navbar-2 form input { 79 | min-width: 80%; 80 | margin: 0; 81 | padding: 8px 10px 8px 32px; 82 | font-size: 13px; 83 | border: solid 1px #0000003e; 84 | border-radius: 3px; 85 | } 86 | 87 | .navbar .navbar-2 form .search-icon { 88 | position: absolute; 89 | left: 25px; 90 | top: 8px; 91 | } 92 | 93 | .navbar .navbar-2 div { 94 | margin: 0 5px 0 0; 95 | } 96 | 97 | .nav-links { 98 | padding: 7px 13px; 99 | border: solid 1px blue; 100 | border-radius: 3px; 101 | background-color: #e7f8fe; 102 | cursor: pointer; 103 | } 104 | 105 | .nav-links:hover { 106 | background-color: #d3e4eb; 107 | } 108 | 109 | @media screen and (min-width: 1600px) { 110 | .navbar { 111 | min-width: unset; 112 | width: 1440px; 113 | } 114 | } 115 | 116 | @media screen and (max-width: 1120px) { 117 | .navbar .navbar-1 form { 118 | display: none; 119 | } 120 | } 121 | 122 | @media screen and (max-width: 760px) { 123 | .slide-in-icon { 124 | display: block; 125 | } 126 | } 127 | 128 | @media screen and (max-width: 950px) { 129 | .navbar .navbar-1 .res-nav { 130 | display: none; 131 | } 132 | } -------------------------------------------------------------------------------- /client/src/components/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect} from 'react' 2 | import { Link, useNavigate } from 'react-router-dom' 3 | import { useSelector, useDispatch } from 'react-redux' 4 | import toast from 'react-hot-toast' 5 | import decode from "jwt-decode"; 6 | 7 | import Avatar from '../Avatar/Avatar' 8 | import { setCurrentUser } from '../../actions/currentUser' 9 | 10 | import logo from '../../assets/logo.png' 11 | import search from '../../assets/search-solid.svg' 12 | import bars from "../../assets/bars-solid.svg"; 13 | import './Navbar.css' 14 | 15 | 16 | const Navbar = ({ setIsOpen }) => { 17 | const dispatch = useDispatch() 18 | const navigate = useNavigate() 19 | const User = useSelector((state) => (state.currentUserReducer)) 20 | 21 | const handleLogout = () => { 22 | dispatch({type: 'LOGOUT'}) 23 | toast.success('Logged out successfully') 24 | navigate('/') 25 | dispatch(setCurrentUser(null)) 26 | } 27 | 28 | useEffect(() => { 29 | const token = User?.token; 30 | if (token) { 31 | const decodedToken = decode(token); 32 | if (decodedToken.exp * 1000 < new Date().getTime()) { 33 | handleLogout(); 34 | } 35 | } 36 | dispatch(setCurrentUser(JSON.parse(localStorage.getItem('Profile')))) 37 | // eslint-disable-next-line 38 | }, [User?.token, dispatch]) 39 | 40 | 41 | return ( 42 | 95 | ) 96 | } 97 | 98 | export default Navbar 99 | -------------------------------------------------------------------------------- /client/src/components/RightSidebar/RightSidebar.css: -------------------------------------------------------------------------------- 1 | .right-sidebar { 2 | float: right; 3 | width: 300px; 4 | margin: 40px 0px 15px 24px; 5 | font-size: 15px; 6 | } 7 | 8 | .widget { 9 | margin-top: 10px; 10 | box-shadow: 3px 3px 10px rgb(0 0 0 / 5%), -3px -3px 10px rgb(0 0 0 / 5%); 11 | } 12 | 13 | .widget h4 { 14 | background-color: #fbf3d5; 15 | margin: 0%; 16 | padding: 15px; 17 | border: solid 1px #f1e5bc; 18 | font-size: 13px; 19 | } 20 | 21 | .right-sidebar-div-1 { 22 | background-color: #fdf7e2; 23 | padding: 15px; 24 | border: solid 1px #f1e5bc; 25 | } 26 | 27 | .right-sidebar-div-1 .right-sidebar-div-2 { 28 | display: flex; 29 | align-items: flex-start; 30 | justify-content: space-evenly; 31 | } 32 | 33 | .right-sidebar-div-1 .right-sidebar-div-2 p { 34 | padding-left: 10px; 35 | margin-top: 0%; 36 | font-size: 13px; 37 | } 38 | 39 | .widget-tags { 40 | margin-top: 40px; 41 | box-shadow: 3px 3px 10px rgb(0 0 0 / 5%), -3px -3px 10px rgb(0 0 0 / 5%); 42 | } 43 | 44 | .widget-tags h4 { 45 | margin: 0%; 46 | padding: 15px; 47 | background-color: #f8f9f9; 48 | border: solid 1px #e3e6e8; 49 | font-size: 13px; 50 | } 51 | 52 | .widget-tags-div { 53 | display: flex; 54 | flex-flow: row wrap; 55 | align-items: center; 56 | justify-content: space-evenly; 57 | border: solid 1px #e3e6e8; 58 | padding: 10px; 59 | } 60 | 61 | .widget-tags-div p { 62 | padding: 5px; 63 | background-color: #e1ecf4; 64 | color: #39739d; 65 | border-radius: 2px; 66 | font-size: 13px; 67 | } 68 | 69 | .widget-tags-div p:hover { 70 | background-color: #e1ecf4ae; 71 | } 72 | 73 | @media screen and (max-width: 1020px) { 74 | .right-sidebar { 75 | display: none; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /client/src/components/RightSidebar/RightSidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./RightSidebar.css"; 3 | import Widget from "./Widget"; 4 | import WidgetTags from "./WidgetTags"; 5 | 6 | const RightSidebar = () => { 7 | return ( 8 | 12 | ); 13 | }; 14 | 15 | export default RightSidebar; 16 | -------------------------------------------------------------------------------- /client/src/components/RightSidebar/Widget.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./RightSidebar.css"; 3 | import comment from "../../assets/comment-alt-solid.svg"; 4 | import pen from "../../assets/pen-solid.svg"; 5 | import blackLogo from "../../assets/blacklogo.svg"; 6 | 7 | const Widget = () => { 8 | return ( 9 |
10 |

The Overflow Blog

11 |
12 |
13 | pen 14 |

15 | Observability is key to the future of software (and your DevOps 16 | career) 17 |

18 |
19 |
20 | pen 21 |

Podcast 374: How valuable is your screen name?

22 |
23 |
24 |

Featured on Meta

25 |
26 |
27 | pen 28 |

Review queue workflows - Final release....

29 |
30 |
31 | pen 32 |

33 | Please welcome Valued Associates: #958 - V2Blast #959 - SpencerG 34 |

35 |
36 |
37 | pen 38 |

39 | Outdated Answers: accepted answer is now unpinned on Stack Overflow 40 |

41 |
42 |
43 |

Hot Meta Posts

44 |
45 |
46 |

38

47 |

48 | Why was this spam flag declined, yet the question marked as spam? 49 |

50 |
51 |
52 |

20

53 |

54 | What is the best course of action when a user has high enough rep 55 | to... 56 |

57 |
58 |
59 |

14

60 |

Is a link to the "How to ask" help page a useful comment?

61 |
62 |
63 |
64 | ); 65 | }; 66 | 67 | export default Widget; 68 | -------------------------------------------------------------------------------- /client/src/components/RightSidebar/WidgetTags.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from 'react-router-dom' 3 | 4 | const WidgetTags = () => { 5 | const tags = [ 6 | "c", 7 | "css", 8 | "express", 9 | "firebase", 10 | "html", 11 | "java", 12 | "javascript", 13 | "mern", 14 | "mongodb", 15 | "mysql", 16 | "next.js", 17 | "node.js", 18 | "php", 19 | "python", 20 | "reactjs", 21 | ]; 22 | 23 | return ( 24 |
25 |

Watched tags

26 |
27 | {tags.map((tag) => ( 28 | 29 |

{tag}

30 | 31 | ))} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default WidgetTags; 38 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Karla:wght@300;400;500;600;700;800&family=Roboto:wght@300;400;500;700&display=swap'); 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | padding: 0; 11 | box-sizing: border-box; 12 | } 13 | 14 | * { 15 | font-family: 'Roboto', sans-serif; 16 | } 17 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import { Provider } from 'react-redux'; 6 | import { createStore, applyMiddleware, compose } from 'redux' 7 | import thunk from 'redux-thunk' 8 | import Reducers from './reducers' 9 | 10 | const store = createStore(Reducers, compose(applyMiddleware(thunk))) 11 | 12 | const root = ReactDOM.createRoot(document.getElementById('root')); 13 | root.render( 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | 21 | 22 | -------------------------------------------------------------------------------- /client/src/pages/AskQuestion/AskQuestion.css: -------------------------------------------------------------------------------- 1 | .ask-question { 2 | min-height: 80vh; 3 | background-color: #f1f2f3; 4 | } 5 | 6 | .ask-ques-container { 7 | margin: auto; 8 | padding: 0px 20px 20px 20px; 9 | max-width: 1200px; 10 | } 11 | 12 | .ask-ques-container h1 { 13 | padding: 80px 0px 20px 0px; 14 | } 15 | 16 | .ask-ques-container form .ask-form-container { 17 | padding: 20px; 18 | background-color: white; 19 | border-radius: 3px; 20 | box-shadow: 0 10px 25px rgb(0 0 0 / 5%), 0 20px 48px rgb(0 0 0 / 5%), 21 | 0 1px 4px rgb(0 0 0 / 10%); 22 | } 23 | 24 | .ask-form-container label h4 { 25 | margin-bottom: 0%; 26 | } 27 | 28 | .ask-form-container label p { 29 | margin: 0%; 30 | font-size: 13px; 31 | padding: 3px 0px; 32 | } 33 | 34 | .ask-form-container label input, 35 | .ask-form-container label textarea { 36 | padding: 10px; 37 | border: solid 1px #0000003e; 38 | font-family: "Roboto", sans-serif; 39 | width: calc(100% - 20px); 40 | resize: none; 41 | } 42 | 43 | .review-btn { 44 | margin: 50px 0px; 45 | padding: 10px; 46 | background-color: #009dff; 47 | border: solid 1px #009dff; 48 | color: white; 49 | border-radius: 4px; 50 | cursor: pointer; 51 | transition: 0.3s; 52 | } 53 | 54 | .review-btn:hover { 55 | background-color: #0086d8; 56 | } 57 | 58 | .ql-editor { 59 | min-height: 150px; 60 | } -------------------------------------------------------------------------------- /client/src/pages/AskQuestion/AskQuestion.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import { useDispatch, useSelector } from 'react-redux' 4 | import { askQuestion } from "../../actions/question"; 5 | import toast from 'react-hot-toast' 6 | 7 | import "./AskQuestion.css"; 8 | import Editor from '../../components/Editor/Editor' 9 | 10 | const AskQuestion = () => { 11 | const [questionTitle, setQuestionTitle] = useState(""); 12 | const [questionBody, setQuestionBody] = useState(""); 13 | const [questionTags, setQuestionTags] = useState(""); 14 | 15 | const navigate = useNavigate(); 16 | const dispatch = useDispatch() 17 | const User = useSelector(state => state.currentUserReducer) 18 | 19 | const handleSubmit = (e) => { 20 | e.preventDefault() 21 | if (User) { 22 | if (questionTitle && questionBody && questionTags) { 23 | dispatch(askQuestion({ 24 | questionTitle, 25 | questionBody, 26 | questionTags, 27 | userPosted: User.result.name, 28 | userId: User?.result._id, 29 | }, navigate)) 30 | toast.success('Question posted successfully') 31 | } else toast.error('Please enter value in all the fields') 32 | } else toast.error('Please Login to ask question') 33 | }; 34 | 35 | return ( 36 |
37 |
38 |

Ask a public Question

39 |
40 |
41 | 56 | 67 | 79 |
80 | 85 |
86 |
87 |
88 | ); 89 | }; 90 | 91 | export default AskQuestion; 92 | -------------------------------------------------------------------------------- /client/src/pages/Auth/AboutAuth.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import './Auth.css' 3 | 4 | const AboutAuth = () => { 5 | return ( 6 |
7 |

Join the Stack Overflow community

8 |

Get unstuck — ask a question

9 |

Unlock new privileges like voting and commenting

10 |

Save your favorite tags, filters, and jobs

11 |

Earn reputation and badges

12 |

13 | Collaborate and share knowledge with a private group for 14 |

15 |

16 | Get Stack Overflow for Teams free for up to 50 users. 17 |

18 |
19 | ); 20 | }; 21 | 22 | export default AboutAuth; -------------------------------------------------------------------------------- /client/src/pages/Auth/Auth.css: -------------------------------------------------------------------------------- 1 | .auth-section { 2 | min-height: 100vh; 3 | margin: 0% auto; 4 | background-color: #f1f2f3; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .auth-container-1 { 11 | padding: 20px; 12 | margin-right: 30px; 13 | } 14 | 15 | .login-logo { 16 | padding: 20px 30px; 17 | } 18 | 19 | .auth-container-2 { 20 | min-width: 20%; 21 | display: flex; 22 | flex-direction: column; 23 | justify-content: center; 24 | align-items: center; 25 | } 26 | 27 | .auth-container-2 form { 28 | width: 100%; 29 | padding: 20px; 30 | background-color: white; 31 | border-radius: 10px; 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: space-evenly; 35 | box-shadow: 0 10px 25px rgb(0 0 0 / 5%), 0 20px 48px rgb(0 0 0 / 5%), 36 | 0 1px 4px rgb(0 0 0 / 10%); 37 | } 38 | 39 | .auth-container-2 form p { 40 | word-wrap: break-word; 41 | } 42 | 43 | .auth-container-2 form label input { 44 | padding: 10px; 45 | width: calc(100% - 30px); 46 | border: solid 1px #0000003e; 47 | font-size: 13px; 48 | } 49 | 50 | .auth-container-2 form label:nth-child(1) h4, 51 | .auth-container-2 form label:nth-child(2) h4, 52 | .auth-container-2 form label:nth-child(3) h4 { 53 | margin-bottom: 5px; 54 | margin-top: 10px; 55 | } 56 | 57 | .auth-container-2 form label:nth-child(4) { 58 | display: flex; 59 | } 60 | 61 | .auth-container-2 form label:nth-child(4) input { 62 | width: 15%; 63 | margin: 13px 0px; 64 | } 65 | 66 | .auth-btn { 67 | margin-top: 10px; 68 | padding: 10px 5px; 69 | background-color: #009dff; 70 | border: solid 1px #009dff; 71 | color: white; 72 | border-radius: 5px; 73 | cursor: pointer; 74 | transition: 0.2s; 75 | font-size: 13px; 76 | font-weight: 500; 77 | } 78 | 79 | .auth-btn:hover { 80 | background-color: #018ce3; 81 | } 82 | 83 | .handle-switch-btn { 84 | background-color: transparent; 85 | color: #007ac6; 86 | border: none; 87 | font-size: 13px; 88 | cursor: pointer; 89 | } 90 | 91 | @media screen and (max-width: 820px) { 92 | .auth-container-1 { 93 | display: none; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /client/src/pages/Auth/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { useDispatch } from 'react-redux' 3 | import { useNavigate } from 'react-router-dom' 4 | import icon from "../../assets/icon.png"; 5 | import AboutAuth from "./AboutAuth"; 6 | import toast from "react-hot-toast"; 7 | import { signup, login } from '../../actions/auth'; 8 | import './Auth.css' 9 | 10 | const Auth = () => { 11 | const [isSignup, setIsSignup] = useState(false); 12 | const [name, setName] = useState(""); 13 | const [email, setEmail] = useState(""); 14 | const [password, setPassword] = useState(""); 15 | 16 | const dispatch = useDispatch() 17 | const navigate = useNavigate() 18 | 19 | const handleSwitch = () => { 20 | setIsSignup(!isSignup); 21 | setName(""); 22 | setEmail(""); 23 | setPassword(""); 24 | } 25 | const handleSubmit = (e) => { 26 | e.preventDefault() 27 | if (!email && !password) { 28 | return toast.error("Please enter email and password"); 29 | } 30 | if (isSignup) { 31 | if (!name) { 32 | return toast.error("Please enter name") 33 | } 34 | dispatch(signup({ name, email, password }, navigate)) 35 | toast.success('Redirecting...') 36 | toast.success('User registered successfully') 37 | toast.success('Logged in successfully') 38 | } else { 39 | dispatch(login({ email, password }, navigate)) 40 | toast.success('Redirecting...') 41 | toast.success('Logged in successfully') 42 | } 43 | 44 | } 45 | 46 | return ( 47 |
48 | {isSignup && } 49 |
50 | stack overflow 51 |
52 | {isSignup && ( 53 | 65 | )} 66 | 78 | 97 | 100 |
101 |

102 | {isSignup ? "Already have an account?" : "Don't have an account?"} 103 | 110 |

111 |
112 |
113 | ) 114 | } 115 | 116 | export default Auth -------------------------------------------------------------------------------- /client/src/pages/Home/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LeftSidebar from '../../components/LeftSidebar/LeftSidebar' 3 | import HomeMainbar from '../../components/HomeMainbar/HomeMainbar' 4 | import RightSidebar from '../../components/RightSidebar/RightSidebar' 5 | import '../../App.css' 6 | 7 | const Home = () => { 8 | return ( 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | ) 17 | } 18 | 19 | export default Home -------------------------------------------------------------------------------- /client/src/pages/Questions/DisplayAnswer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useDispatch, useSelector } from 'react-redux' 3 | import { Link, useParams } from 'react-router-dom' 4 | import Avatar from '../../components/Avatar/Avatar' 5 | import moment from 'moment' 6 | import { deleteAnswer } from "../../actions/question"; 7 | import toast from 'react-hot-toast' 8 | import HTMLReactParser from 'html-react-parser' 9 | 10 | const DisplayAnswer = ({ question, handleShare }) => { 11 | 12 | const dispatch = useDispatch() 13 | const User = useSelector((state) => state.currentUserReducer); 14 | const { id } = useParams() 15 | 16 | const handleDelete = (answerId, noOfAnswers) => { 17 | dispatch(deleteAnswer(id, answerId, noOfAnswers - 1)); 18 | toast.success('Answer deleted') 19 | } 20 | return ( 21 |
22 | {question.answer.map((ans) => ( 23 |
24 |

{HTMLReactParser(ans.answerBody)}

25 |
26 |
27 | 30 | {User?.result?._id === ans?.userId && ( 31 | 37 | )} 38 |
39 |
40 |

answered {moment(ans.answeredOn).fromNow()}

41 | 46 | 52 | {ans.userAnswered.charAt(0).toUpperCase()} 53 | 54 |
{ans.userAnswered}
55 | 56 |
57 |
58 |
59 | ))} 60 |
61 | ) 62 | } 63 | 64 | export default DisplayAnswer -------------------------------------------------------------------------------- /client/src/pages/Questions/DisplayQuestion.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import LeftSidebar from "../../components/LeftSidebar/LeftSidebar"; 3 | import RightSidebar from "../../components/RightSidebar/RightSidebar"; 4 | import QuestionsDetails from "./QuestionsDetails"; 5 | 6 | 7 | const DisplayQuestion = () => { 8 | return ( 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default DisplayQuestion; 20 | -------------------------------------------------------------------------------- /client/src/pages/Questions/Questions.css: -------------------------------------------------------------------------------- 1 | .question-details-page { 2 | width: calc(100% - 300px - 24px); 3 | float: left; 4 | margin: 25px 0px; 5 | padding: 0px; 6 | } 7 | 8 | .question-details-container { 9 | margin-bottom: 20px; 10 | padding-bottom: 20px; 11 | border-bottom: solid 1px rgba(0, 0, 0, 0.112); 12 | } 13 | 14 | .question-details-container-2 { 15 | display: flex; 16 | } 17 | 18 | .question-votes { 19 | padding: 5px 20px 5px 10px; 20 | } 21 | 22 | .question-votes p { 23 | margin: 0%; 24 | font-size: 25px; 25 | text-align: center; 26 | } 27 | 28 | .votes-icon { 29 | font-size: 40px; 30 | cursor: pointer; 31 | color: rgb(206, 203, 203); 32 | } 33 | 34 | .votes-icon:active { 35 | color: #ef8236; 36 | } 37 | 38 | .question-details-container .question-body { 39 | line-height: 22px; 40 | white-space: pre-line; 41 | } 42 | 43 | .question-details-container .question-details-tags { 44 | display: flex; 45 | align-items: center; 46 | justify-content: flex-start; 47 | } 48 | 49 | .question-details-container .question-details-tags p, 50 | .post-ans-container p .ans-tags { 51 | padding: 5px 5px; 52 | margin: 3px; 53 | font-size: 13px; 54 | border-radius: 2px; 55 | background-color: #e1ecf4; 56 | color: #39739d; 57 | text-decoration: none; 58 | line-height: 22px; 59 | } 60 | 61 | .post-ans-container p .ans-tags:hover { 62 | background-color: #e1ecf4ae; 63 | } 64 | 65 | .question-actions-user { 66 | width: 100%; 67 | display: flex; 68 | align-items: center; 69 | justify-content: space-between; 70 | } 71 | 72 | .question-actions-user button, 73 | .edit-question-btn { 74 | background-color: transparent; 75 | border: none; 76 | padding: 5px 0px; 77 | margin: 0px 10px 0px 0px; 78 | text-decoration: none; 79 | color: #939292; 80 | cursor: pointer; 81 | font-size: 14px; 82 | transition: 0.3s; 83 | } 84 | 85 | .question-actions-user button:active { 86 | border-bottom: solid 2px black; 87 | } 88 | 89 | .question-actions-user div:nth-child(2) p, 90 | .question-actions-user div:nth-child(2) .user-link { 91 | text-decoration: none; 92 | font-size: 14px; 93 | margin: 0%; 94 | } 95 | 96 | .user-link { 97 | display: flex; 98 | align-items: center; 99 | } 100 | 101 | .user-link div { 102 | padding-left: 10px; 103 | } 104 | 105 | /* post answer container */ 106 | 107 | .post-ans-container form textarea { 108 | padding: 10px; 109 | border: solid 1px rgba(0, 0, 0, 0.3); 110 | font-family: "Roboto", sans-serif; 111 | width: calc(100% - 20px); 112 | resize: vertical; 113 | } 114 | 115 | .post-ans-container form .post-ans-btn { 116 | margin: 20px 0px; 117 | padding: 10px 10px; 118 | background-color: #009dff; 119 | color: white; 120 | border: solid 1px #009dff; 121 | border-radius: 4px; 122 | cursor: pointer; 123 | transition: 0.5s all; 124 | } 125 | 126 | .post-ans-container form .post-ans-btn:hover { 127 | background-color: #0086d8; 128 | } 129 | 130 | /* Display answer container */ 131 | 132 | .display-ans { 133 | padding-bottom: 20px; 134 | border-bottom: solid 1px rgba(0, 0, 0, 0.112); 135 | } 136 | 137 | .display-ans p { 138 | font-size: 14px; 139 | line-height: 18px; 140 | white-space: pre-line; 141 | } 142 | 143 | @media screen and (max-width: 1020px) { 144 | .question-details-page { 145 | width: calc(100% - 24px); 146 | } 147 | } 148 | 149 | .ql-editor { 150 | min-height: 150px; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /client/src/pages/Questions/Questions.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import LeftSidebar from '../../components/LeftSidebar/LeftSidebar' 3 | import HomeMainbar from '../../components/HomeMainbar/HomeMainbar' 4 | import RightSidebar from '../../components/RightSidebar/RightSidebar' 5 | import '../../App.css' 6 | 7 | const Questions = () => { 8 | return ( 9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | ) 17 | } 18 | 19 | export default Questions -------------------------------------------------------------------------------- /client/src/pages/Questions/QuestionsDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useParams, Link, useNavigate, useLocation } from "react-router-dom"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import moment from "moment"; 5 | import copy from "copy-to-clipboard"; 6 | import toast from 'react-hot-toast' 7 | import HTMLReactParser from 'html-react-parser' 8 | 9 | 10 | import upvote from "../../assets/sort-up.svg"; 11 | import downvote from "../../assets/sort-down.svg"; 12 | import "./Questions.css"; 13 | import Avatar from "../../components/Avatar/Avatar"; 14 | import DisplayAnswer from "./DisplayAnswer"; 15 | import { 16 | postAnswer, 17 | deleteQuestion, 18 | voteQuestion 19 | } from "../../actions/question"; 20 | import Editor from "../../components/Editor/Editor"; 21 | import Loader from "../../components/Loader/Loader"; 22 | 23 | 24 | const QuestionsDetails = () => { 25 | const { id } = useParams(); 26 | const questionsList = useSelector((state) => state.questionsReducer); 27 | const User = useSelector((state) => state.currentUserReducer); 28 | 29 | const [answer, setAnswer] = useState(""); 30 | const navigate = useNavigate(); 31 | const dispatch = useDispatch(); 32 | const location = useLocation(); 33 | const url = "https://stack-overflow-clone-gautam.vercel.app/"; 34 | 35 | const handlePostAns = (e, answerLength) => { 36 | e.preventDefault(); 37 | if (!User) { 38 | toast.error("Please Login or Signup to answer a question"); 39 | navigate("/Auth"); 40 | } else { 41 | if (answer === "") { 42 | toast.error("Enter an answer before submitting"); 43 | } else { 44 | dispatch( 45 | postAnswer({ 46 | id, 47 | noOfAnswers: answerLength + 1, 48 | answerBody: answer, 49 | userAnswered: User.result.name, 50 | userId: User.result._id 51 | }) 52 | ); 53 | setAnswer(""); 54 | } 55 | } 56 | }; 57 | 58 | const handleShare = () => { 59 | copy(url + location.pathname); 60 | toast.success('URL copied to clipboard'); 61 | }; 62 | 63 | const handleDelete = () => { 64 | dispatch(deleteQuestion(id, navigate)); 65 | toast.success('Question deleted') 66 | }; 67 | 68 | const handleUpVote = () => { 69 | if (!User) { 70 | return toast.error("Please Login or Signup to upvote"); 71 | } 72 | dispatch(voteQuestion(id, 'upVote', User.result._id)) 73 | toast.success('Upvoted') 74 | }; 75 | 76 | const handleDownVote = () => { 77 | if (!User) { 78 | return toast.error("Please Login or Signup to downvote"); 79 | } 80 | dispatch(voteQuestion(id, 'downVote', User.result._id)) 81 | toast.success('Downvoted') 82 | }; 83 | 84 | return ( 85 |
86 | {questionsList.data === null ? ( 87 | 88 | ) : ( 89 | <> 90 | {questionsList.data 91 | .filter((question) => question._id === id) 92 | .map((question) => ( 93 |
94 |
95 |

{question.questionTitle}

96 |
97 |
98 | 105 |

{question.upVote.length - question.downVote.length}

106 | 113 |
114 |
115 |

{HTMLReactParser(question.questionBody)}

116 |
117 | {question.questionTags.map((tag) => ( 118 |
{tag}
119 | ))} 120 |
121 |
122 |
123 | 126 | {User?.result._id === question.userId && ( 127 | 130 | )} 131 |
132 |
133 |

asked {moment(question.askedOn).fromNow()}

134 | 139 | 145 | {question.userPosted.charAt(0).toUpperCase()} 146 | 147 |
{question.userPosted}
148 | 149 |
150 |
151 |
152 |
153 |
154 | {question.noOfAnswers !== 0 && ( 155 |
156 |

{question.noOfAnswers} Answers

157 | 162 |
163 | )} 164 |
165 |

Your Answer

166 |
{ 168 | handlePostAns(e, question.answer.length); 169 | }} 170 | > 171 |
172 | 176 |
177 |
178 | 183 |
184 |

185 | Browse other Question tagged 186 | {question.questionTags.map((tag) => ( 187 | 188 | {" "} 189 | {tag}{" "} 190 | 191 | ))}{" "} 192 | or 193 | 197 | {" "} 198 | ask your own question. 199 | 200 |

201 |
202 |
203 | ))} 204 | 205 | )} 206 |
207 | ); 208 | }; 209 | 210 | export default QuestionsDetails; 211 | -------------------------------------------------------------------------------- /client/src/pages/Tags/Tags.css: -------------------------------------------------------------------------------- 1 | .tags-h1 { 2 | margin-top: 50px; 3 | font-weight: 400; 4 | } 5 | 6 | .tags-p { 7 | margin: 0px; 8 | font-size: 15px; 9 | } 10 | 11 | .tags-list-container { 12 | padding: 30px 0px; 13 | display: grid; 14 | grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); 15 | gap: 10px; 16 | } 17 | 18 | .tag { 19 | padding: 10px; 20 | border: solid 1px #d2d2d2; 21 | border-radius: 2px; 22 | } 23 | 24 | .tag h5 { 25 | display: inline-block; 26 | margin: 10px 0px; 27 | padding: 5px 5px; 28 | background-color: #e1ecf4; 29 | color: #39739d; 30 | } 31 | 32 | .tag p { 33 | font-size: 14px; 34 | color: #323232; 35 | line-height: 17px; 36 | } 37 | -------------------------------------------------------------------------------- /client/src/pages/Tags/Tags.jsx: -------------------------------------------------------------------------------- 1 | import LeftSidebar from "../../components/LeftSidebar/LeftSidebar"; 2 | import TagsList from "./TagsList"; 3 | import { tagsList } from "./tagList"; 4 | import "./Tags.css"; 5 | 6 | const Tags = () => { 7 | return ( 8 |
9 | 10 |
11 |

12 | Tags 13 |

14 |

15 | A tag is a keyword or label that categorizes your question with other, 16 | similar questions. 17 |

18 |

19 | Using the right tags makes it easier for others to find and answer 20 | your question. 21 |

22 |
23 | {tagsList.map((tag) => ( 24 | 25 | ))} 26 |
27 |
28 |
29 | ) 30 | } 31 | 32 | export default Tags -------------------------------------------------------------------------------- /client/src/pages/Tags/TagsList.jsx: -------------------------------------------------------------------------------- 1 | import "./Tags.css"; 2 | 3 | const TagsList = ({ tag }) => { 4 | return ( 5 |
6 |
{tag.tagName}
7 |

{tag.tagDesc}

8 |
9 | ); 10 | }; 11 | 12 | export default TagsList; 13 | -------------------------------------------------------------------------------- /client/src/pages/Tags/tagList.js: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid' 2 | 3 | export const tagsList = [ 4 | { 5 | id: nanoid(), 6 | tagName: "javascript", 7 | tagDesc: 8 | "For questions regarding programming in ECMAScript (JavaScript/JS) and its various dialects/implementations (excluding ActionScript). Please include all relevant tags on your question;", 9 | }, 10 | { 11 | id: nanoid(), 12 | tagName: "python", 13 | tagDesc: 14 | "Python is a multi-paradigm, dynamically typed, multipurpose programming language. It is designed to be quick to learn, understand, and use, and enforces a clean and uniform syntax.", 15 | }, 16 | { 17 | id: nanoid(), 18 | tagName: "c#", 19 | tagDesc: 20 | "C# (pronounced 'see sharp') is a high level, statically typed, multi-paradigm programming language developed by Microsoft", 21 | }, 22 | { 23 | id: nanoid(), 24 | tagName: "java", 25 | tagDesc: 26 | "Java is a high-level object oriented programming language. Use this tag when you're having problems using or understanding the language itself. ", 27 | }, 28 | { 29 | id: nanoid(), 30 | tagName: "php", 31 | tagDesc: 32 | "PHP is a widely used, open source, general-purpose, multi-paradigm, dynamically typed and interpreted scripting language originally designed for server-side web development", 33 | }, 34 | { 35 | id: nanoid(), 36 | tagName: "html", 37 | tagDesc: 38 | "HTML (HyperText Markup Language) is the markup language for creating web pages and other information to be displayed in a web browser.", 39 | }, 40 | { 41 | id: nanoid(), 42 | tagName: "android", 43 | tagDesc: 44 | "Android is Google's mobile operating system, used for programming or developing digital devices (Smartphones, Tablets, Automobiles, TVs, Wear, Glass, IoT).", 45 | }, 46 | { 47 | id: nanoid(), 48 | tagName: "css", 49 | tagDesc: 50 | "CSS is a representation style sheet language used for describing the look and formatting of HTML, XML documents and SVG elements including colors, layout, fonts, and animations", 51 | }, 52 | { 53 | id: nanoid(), 54 | tagName: "Reactjs", 55 | tagDesc: 56 | "React is a JavaScript library for building user interfaces. It uses a declarative, component-based paradigm and aims to be both efficient and flexible.", 57 | }, 58 | { 59 | id: nanoid(), 60 | tagName: "node.js", 61 | tagDesc: 62 | "Node.js is an event-based, non-blocking, asynchronous I/O runtime that uses Google's V8 JavaScript engine and libuv library. ", 63 | }, 64 | ]; 65 | -------------------------------------------------------------------------------- /client/src/pages/UserProfile/EditProfileForm.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useDispatch } from "react-redux"; 3 | import { updateProfile } from "../../actions/users"; 4 | import toast from 'react-hot-toast' 5 | import Editor from "../../components/Editor/Editor"; 6 | 7 | const EditProfileForm = ({ currentUser, setSwitch }) => { 8 | const [name, setName] = useState(currentUser?.result?.name); 9 | const [about, setAbout] = useState(currentUser?.result?.about); 10 | const [tags, setTags] = useState([]); 11 | const dispatch = useDispatch(); 12 | 13 | const handleSubmit = (e) => { 14 | e.preventDefault(); 15 | if (tags[0] === "" || tags.length === 0) { 16 | toast.error("Update tags field"); 17 | } else { 18 | dispatch(updateProfile(currentUser?.result?._id, { name, about, tags })); 19 | toast.success('Profile updated') 20 | } 21 | setSwitch(false); 22 | }; 23 | 24 | return ( 25 |
26 |

Edit Your Profile

27 |

Public information

28 |
29 | 37 | 44 | 53 |
54 | 55 | 62 |
63 |
64 | ); 65 | }; 66 | 67 | export default EditProfileForm; 68 | -------------------------------------------------------------------------------- /client/src/pages/UserProfile/ProfileBio.jsx: -------------------------------------------------------------------------------- 1 | import HTMLReactParser from "html-react-parser"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const ProfileBio = ({ currentProfile }) => { 5 | return ( 6 |
7 |
8 | {currentProfile?.tags.length !== 0 ? ( 9 | <> 10 |

Tags watched

11 |
12 | {currentProfile?.tags.map((tag) => ( 13 | 14 |

{tag}

15 | 16 | ))} 17 |
18 | 19 | ) : ( 20 |

0 tags watched

21 | )} 22 |
23 |
24 | {currentProfile?.about ? ( 25 | <> 26 |

About

27 |

{HTMLReactParser(currentProfile?.about)}

28 | 29 | ) : ( 30 |

No bio found

31 | )} 32 |
33 |
34 | ); 35 | }; 36 | 37 | export default ProfileBio; 38 | -------------------------------------------------------------------------------- /client/src/pages/UserProfile/UserProfile.css: -------------------------------------------------------------------------------- 1 | .user-details-container { 2 | width: 100%; 3 | margin-top: 50px; 4 | display: flex; 5 | align-items: flex-start; 6 | justify-content: space-between; 7 | } 8 | 9 | .user-details { 10 | display: flex; 11 | align-items: flex-start; 12 | } 13 | 14 | .user-name { 15 | padding-left: 20px; 16 | } 17 | .user-name p { 18 | color: #7e7e7e; 19 | } 20 | .edit-profile-btn { 21 | padding: 8px 10px; 22 | border: solid 1px #7e7e7e; 23 | border-radius: 2px; 24 | background-color: white; 25 | cursor: pointer; 26 | transition: 0.3s; 27 | } 28 | .edit-profile-btn:hover { 29 | background-color: #f5f9fc; 30 | } 31 | .edit-profile-title { 32 | padding: 20px 0px; 33 | border-bottom: solid 1px #dbd9d9; 34 | } 35 | .edit-profile-title-2 { 36 | color: grey; 37 | font-weight: 400; 38 | } 39 | .edit-profile-form { 40 | padding: 20px; 41 | border: solid 1px #dbd9d9; 42 | border-radius: 5px; 43 | } 44 | .edit-profile-form label h3 { 45 | margin: 0%; 46 | padding: 3px 0px; 47 | } 48 | .edit-profile-form label p { 49 | margin: 0%; 50 | padding: 3px 0px; 51 | } 52 | .edit-profile-form label input, 53 | .edit-profile-form label textarea { 54 | padding: 5px; 55 | margin-bottom: 20px; 56 | border: solid 1px #dbd9d9; 57 | width: 50%; 58 | } 59 | 60 | .user-submit-btn { 61 | padding: 14px 10px; 62 | background-color: #0a95ff; 63 | color: white; 64 | border: none; 65 | border-radius: 5px; 66 | transition: 0.2s; 67 | cursor: pointer; 68 | } 69 | 70 | .user-submit-btn:hover { 71 | background-color: #0074cc; 72 | } 73 | 74 | .user-cancel-btn { 75 | padding: 14px 10px; 76 | color: #0a95ff; 77 | background-color: transparent; 78 | border: none; 79 | margin-left: 10px; 80 | cursor: pointer; 81 | } 82 | 83 | .user-tags-container{ 84 | display: flex; 85 | } 86 | 87 | .user-tags-link{ 88 | text-decoration: none; 89 | padding: 0 5px; 90 | margin: 5px; 91 | background-color: #e1ecf4; 92 | color: #39739d; 93 | border-radius: 2px; 94 | font-size: 13px; 95 | } 96 | -------------------------------------------------------------------------------- /client/src/pages/UserProfile/UserProfile.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { useParams } from "react-router"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faBirthdayCake, faPen } from "@fortawesome/free-solid-svg-icons"; 6 | import moment from "moment"; 7 | 8 | import LeftSidebar from "../../components/LeftSidebar/LeftSidebar"; 9 | import Avatar from "../../components/Avatar/Avatar"; 10 | import EditProfileForm from "./EditProfileForm"; 11 | import ProfileBio from "./ProfileBio"; 12 | import "./UserProfile.css"; 13 | 14 | const UserProfile = () => { 15 | const { id } = useParams() 16 | const users = useSelector((state) => state.usersReducer); 17 | const currentProfile = users.filter((user) => user._id === id)[0]; 18 | const currentUser = useSelector((state) => state.currentUserReducer); 19 | const [Switch, setSwitch] = useState(false); 20 | return ( 21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | 35 | {currentProfile?.name.charAt(0).toUpperCase()} 36 | 37 |
38 |

{currentProfile?.name}

39 |

40 | Joined{" "} 41 | {moment(currentProfile?.joinedOn).fromNow()} 42 |

43 |
44 |
45 | {currentUser?.result._id === id && ( 46 | 53 | )} 54 |
55 | <> 56 | {Switch ? ( 57 | 61 | ) : ( 62 | 63 | )} 64 | 65 |
66 |
67 |
68 |
69 | ) 70 | } 71 | 72 | export default UserProfile -------------------------------------------------------------------------------- /client/src/pages/Users/User.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom' 2 | import './Users.css' 3 | 4 | const User = ({ user }) => { 5 | return ( 6 | 7 |

{user.name.charAt(0).toUpperCase()}

8 |
{user.name}
9 | 10 | ) 11 | } 12 | 13 | export default User -------------------------------------------------------------------------------- /client/src/pages/Users/Users.css: -------------------------------------------------------------------------------- 1 | .user-list-container { 2 | padding: 30px 0px; 3 | display: grid; 4 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 5 | gap: 30px; 6 | } 7 | 8 | .user-profile-link { 9 | display: flex; 10 | align-items: center; 11 | justify-content: flex-start; 12 | text-decoration: none; 13 | color: black; 14 | } 15 | 16 | .user-profile-link h3 { 17 | padding: 10px 13px; 18 | background-color: #d3d3d3; 19 | border-radius: 50%; 20 | } 21 | 22 | .user-profile-link h5 { 23 | margin: 0px 10px; 24 | } 25 | -------------------------------------------------------------------------------- /client/src/pages/Users/Users.jsx: -------------------------------------------------------------------------------- 1 | import LeftSidebar from "../../components/LeftSidebar/LeftSidebar" 2 | import User from './User' 3 | import { useSelector } from "react-redux" 4 | 5 | import './Users.css' 6 | import Loader from "../../components/Loader/Loader" 7 | 8 | const Users = () => { 9 | const users = useSelector(state => state.usersReducer) 10 | return ( 11 |
12 | 13 |
14 |

Users

15 | { 16 | users.length === 0 ? ( 17 | 18 | ) : ( 19 |
20 | {users.map(user => ( 21 | 22 | ))} 23 |
24 | ) 25 | } 26 |
27 |
28 | ) 29 | } 30 | 31 | export default Users -------------------------------------------------------------------------------- /client/src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | const authReducer = (state= { data: null }, actions) => { 2 | switch (actions.type) { 3 | case 'AUTH': 4 | localStorage.setItem('Profile', JSON.stringify({...actions?.data})) 5 | return {...state, data: actions?.data} 6 | case 'LOGOUT': 7 | localStorage.removeItem('Profile') 8 | return { ...state, data: null} 9 | 10 | default: 11 | return state; 12 | } 13 | } 14 | 15 | export default authReducer 16 | 17 | -------------------------------------------------------------------------------- /client/src/reducers/currentUser.js: -------------------------------------------------------------------------------- 1 | const currentUserReducer = (state= null, actions) => { 2 | switch (actions.type) { 3 | case 'FETCH_CURRENT_USER': 4 | return actions.payload 5 | default: 6 | return state; 7 | } 8 | } 9 | 10 | export default currentUserReducer -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import authReducer from "./auth"; 3 | import currentUserReducer from "./currentUser"; 4 | import questionsReducer from "./questions"; 5 | import usersReducer from "./users"; 6 | 7 | export default combineReducers({ 8 | authReducer, 9 | currentUserReducer, 10 | questionsReducer, 11 | usersReducer, 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/reducers/questions.js: -------------------------------------------------------------------------------- 1 | const questionsReducer = (state = { data: null }, action) => { 2 | switch (action.type) { 3 | 4 | case "POST_QUESTION": 5 | return { ...state }; 6 | 7 | case "POST_ANSWER": 8 | return { ...state }; 9 | 10 | case "FETCH_ALL_QUESTIONS": 11 | return { ...state, data: action.payload }; 12 | 13 | default: 14 | return state; 15 | } 16 | }; 17 | export default questionsReducer; 18 | -------------------------------------------------------------------------------- /client/src/reducers/users.js: -------------------------------------------------------------------------------- 1 | const usersReducer = (states = [], action) => { 2 | switch (action.type) { 3 | case "FETCH_USERS": 4 | return action.payload; 5 | case "UPDATE_CURRENT_USER": 6 | return states.map(state => 7 | state._id === action.payload._id ? action.payload : state 8 | ); 9 | default: 10 | return states; 11 | } 12 | }; 13 | 14 | export default usersReducer; -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /server/config/connectDB.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connectDB = async () => { 4 | try { 5 | await mongoose.connect(process.env.MONGO_URI, { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true 8 | }) 9 | console.log(`Connected to MongoDB successfully`.bgGreen.white) 10 | } catch (error) { 11 | console.log(`MongoDB Error: ${error}`.bgRed.white) 12 | } 13 | } 14 | 15 | export default connectDB -------------------------------------------------------------------------------- /server/controllers/answer.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Questions from "../models/questions.js"; 3 | 4 | export const postAnswer = async (req, res) => { 5 | const { id: _id } = req.params 6 | const { noOfAnswers, answerBody, userAnswered, userId } = req.body; 7 | 8 | if(!mongoose.Types.ObjectId.isValid(_id)) { 9 | return res.status(404).send('Question unavailable...') 10 | } 11 | 12 | updateNoofQuestions(_id, noOfAnswers) 13 | try { 14 | const updatedQuestion = await Questions.findByIdAndUpdate(_id, { 15 | $addToSet: { 16 | answer: [{ 17 | answerBody, userAnswered, userId 18 | }] 19 | }}) 20 | res.status(200).json(updatedQuestion); 21 | } catch (error) { 22 | res.status(400).json("Error while updating"); 23 | } 24 | } 25 | 26 | const updateNoofQuestions = async (_id, noOfAnswers) => { 27 | try { 28 | await Questions.findByIdAndUpdate(_id, { 29 | $set: { 30 | noOfAnswers: noOfAnswers 31 | } 32 | }) 33 | } catch (error) { 34 | console.log(error) 35 | } 36 | } 37 | 38 | export const deleteAnswer = async (req, res) => { 39 | const { id: _id } = req.params 40 | const { answerId, noOfAnswers } = req.body 41 | 42 | if (!mongoose.Types.ObjectId.isValid(_id)) { 43 | return res.status(404).send("Question unavailable..."); 44 | } 45 | 46 | if (!mongoose.Types.ObjectId.isValid(answerId)) { 47 | return res.status(404).send("Answer unavailable..."); 48 | } 49 | 50 | updateNoofQuestions(_id, noOfAnswers) 51 | 52 | try { 53 | await Questions.updateOne( 54 | {_id}, 55 | { $pull: { 56 | // pulls out the answer with same id 57 | answer: { _id: answerId} 58 | }} 59 | ) 60 | res.status(200).json({ message: "Successfully deleted..." }); 61 | } catch (error) { 62 | res.status(405).json(error); 63 | } 64 | } -------------------------------------------------------------------------------- /server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import bcrypt from 'bcryptjs' 3 | 4 | import Users from '../models/auth.js' 5 | 6 | export const signUpController = async (req, res) => { 7 | const { name, email, password } = req.body 8 | try { 9 | const existingUser = await Users.findOne({ email }) 10 | if (existingUser) { 11 | return res.status(404).json({ 12 | message: "User already exists." 13 | }) 14 | } 15 | 16 | const hashedPassword = await bcrypt.hash(password, 10) 17 | const newUser = await Users.create({ 18 | name, email, password: hashedPassword 19 | }) 20 | const token = jwt.sign({ 21 | email: newUser.email, 22 | id: newUser._id 23 | }, 24 | process.env.JWT_SECRET, 25 | { expiresIn: '1h' } 26 | ) 27 | 28 | res.status(200).json({ 29 | result: newUser, token 30 | }) 31 | } catch (error) { 32 | res.status(500).json("Something went wrong...") 33 | } 34 | } 35 | 36 | 37 | export const logInController = async (req, res) => { 38 | const { email, password } = req.body 39 | try { 40 | const existingUser = await Users.findOne({ email }) 41 | if (!existingUser) { 42 | return res.status(404).json({ 43 | message: "User doesn't exists" 44 | })} 45 | 46 | const isPasswordCorrect = await bcrypt.compare(password, existingUser.password) 47 | 48 | if (!isPasswordCorrect) { 49 | return res.status(404).json({ 50 | message: "Invalid Credentials" 51 | }) 52 | } 53 | 54 | const token = jwt.sign({ 55 | email: existingUser.email, 56 | id: existingUser._id 57 | }, 58 | process.env.JWT_SECRET, 59 | { expiresIn: '1h' } 60 | ) 61 | 62 | res.status(200).json({ 63 | result: existingUser, token 64 | }) 65 | } catch (error) { 66 | res.status(500).json("Something went wrong...") 67 | } 68 | } -------------------------------------------------------------------------------- /server/controllers/chatbot.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config() 3 | import { Configuration, OpenAIApi } from 'openai' 4 | const configuration = new Configuration({ 5 | apiKey: process.env.OPEN_AI_KEY, 6 | }); 7 | const openai = new OpenAIApi(configuration); 8 | 9 | export const chatbotController = async (req, res) => { 10 | const { message } = req.body 11 | try { 12 | const response = await openai.createCompletion({ 13 | model: "text-davinci-003", 14 | prompt: `${message}`, 15 | max_tokens: 100, 16 | temperature: 0, 17 | }); 18 | res.status(200).json({ 19 | message: response.data.choices[0].text 20 | }) 21 | } catch (error) { 22 | console.log(error) 23 | res.status(400).json("Something went wrong...") 24 | } 25 | } -------------------------------------------------------------------------------- /server/controllers/otp.js: -------------------------------------------------------------------------------- 1 | import OTP from '../models/otp.js' 2 | import generateOTP from '../utils/generateOTP.js' 3 | import sendEmail from '../utils/sendEmail.js' 4 | import { hashData, verifyHashedData } from '../utils/hashData.js' 5 | const { AUTH_EMAIL } = process.env 6 | 7 | const sendOTPController = async (req, res) => { 8 | try { 9 | const { email, subject, message, duration } = req.body 10 | const createdOTP = await sendOTP({ 11 | email, 12 | subject, 13 | message, 14 | duration 15 | }) 16 | res.status(200).json(createdOTP) 17 | } catch (error) { 18 | res.status(400).send(error.message) 19 | } 20 | } 21 | 22 | const verifyOTPController = async (req, res) => { 23 | try { 24 | let { email, otp } = req.body 25 | const validOTP = await verifyOTP({ email, otp }) 26 | res.status(200).json({ valid: validOTP }) 27 | } catch (error) { 28 | res.status(400).send(error.message) 29 | } 30 | } 31 | 32 | const verifyOTP = async ({ email, otp }) => { 33 | try { 34 | if(!(email && otp)) { 35 | throw Error('Provide values for email, otp') 36 | } 37 | 38 | // ensure otp record exists 39 | const matchedOTPRecord = await OTP.findOne({ email }) 40 | if(!matchedOTPRecord) { 41 | throw Error('No otp record found') 42 | } 43 | 44 | const { expiresAt } = matchedOTPRecord 45 | 46 | // checking for expired code 47 | if(expiresAt < Date.now()) { 48 | await OTP.deleteOne({ email }) 49 | throw Error('Code has expired. Request for a new one') 50 | } 51 | 52 | // not expired yet, verify value 53 | const hashedOTP = matchedOTPRecord.otp 54 | const validOTP = await verifyHashedData(otp, hashedOTP) 55 | return validOTP 56 | 57 | } catch (error) { 58 | throw(error) 59 | } 60 | } 61 | 62 | const sendOTP = async ({ email, subject, message, duration = 1 }) => { 63 | try { 64 | if(!(email && subject && message)) { 65 | throw Error('Provide values for email, subject, message') 66 | } 67 | 68 | // clear any old record 69 | await OTP.deleteOne({ email }) 70 | 71 | //generate pin 72 | const generatedOTP = await generateOTP() 73 | 74 | // send email 75 | const mailOptions = { 76 | from: AUTH_EMAIL, 77 | to: email, 78 | subject, 79 | html: `

${message}

80 |

81 | ${generatedOTP} 82 |

83 |

This code 84 | expires in ${duration} hour(s). 85 |

`, 86 | } 87 | await sendEmail(mailOptions) 88 | 89 | //save otp record 90 | const hashedOTP = await hashData(generatedOTP) 91 | const newOTP = await new OTP({ 92 | email, 93 | otp: hashedOTP, 94 | createdAt: Date.now(), 95 | expiresAt: Date.now() + 3600000 * + duration, 96 | }) 97 | 98 | const createdOTPRecord = await newOTP.save() 99 | return createdOTPRecord 100 | 101 | } catch (error) { 102 | throw(error) 103 | } 104 | } 105 | 106 | const deleteOTP = async (email) => { 107 | try { 108 | await OTP.deleteOne({ email }) 109 | } catch (error) { 110 | throw error 111 | } 112 | } 113 | 114 | export { sendOTPController, verifyOTPController } 115 | 116 | -------------------------------------------------------------------------------- /server/controllers/questions.js: -------------------------------------------------------------------------------- 1 | import Questions from "../models/questions.js"; 2 | import mongoose from "mongoose"; 3 | 4 | export const askQuestion = async (req, res) => { 5 | const postQuestionData = req.body 6 | const postQuestion = new Questions(postQuestionData) 7 | 8 | try { 9 | await postQuestion.save() 10 | res.status(200).json('Question posted successfully') 11 | } catch (error) { 12 | console.log(error) 13 | res.status(409).json("Couldn't post a new question"); 14 | } 15 | } 16 | 17 | export const getAllQuestions = async (req, res) => { 18 | try { 19 | const questionList = await Questions.find().sort({ askedOn: -1 }); 20 | res.status(200).json(questionList); 21 | } catch (error) { 22 | res.status(404).json({ message: error.message }); 23 | } 24 | }; 25 | 26 | export const deleteQuestion = async (req, res) => { 27 | const { id: _id } = req.params; 28 | 29 | if (!mongoose.Types.ObjectId.isValid(_id)) { 30 | return res.status(404).send("Question unavailable..."); 31 | } 32 | 33 | try { 34 | await Questions.findByIdAndRemove(_id); 35 | res.status(200).json({ message: "Question successfully deleted..."}); 36 | } catch (error) { 37 | res.status(404).json({ message: error.message }); 38 | } 39 | }; 40 | 41 | export const voteQuestion = async (req, res) => { 42 | const { id: _id } = req.params; 43 | const { value, userId } = req.body; 44 | 45 | if (!mongoose.Types.ObjectId.isValid(_id)) { 46 | return res.status(404).send("Question unavailable..."); 47 | } 48 | 49 | try { 50 | const question = await Questions.findById(_id); 51 | /* 52 | When a user votes on a question, the function checks if the user has already upvoted or downvoted the question by checking if their userId is in the respective array. If the user has already voted in the opposite way (e.g. they previously downvoted but now want to upvote), their userId is removed from the opposite array. If the user has not already voted in the same way (e.g. they have not already upvoted and now want to upvote), their userId is added to the respective array. If the user has already voted in the same way, their userId is removed from the respective array, effectively canceling their previous vote. 53 | */ 54 | const upIndex = question.upVote.findIndex((id) => id === String(userId)); 55 | const downIndex = question.downVote.findIndex( 56 | (id) => id === String(userId) 57 | ); 58 | 59 | if (value === "upVote") { 60 | if (downIndex !== -1) { 61 | question.downVote = question.downVote.filter( 62 | (id) => id !== String(userId) 63 | ); 64 | } 65 | 66 | if (upIndex === -1) { 67 | question.upVote.push(userId); 68 | } else { 69 | question.upVote = question.upVote.filter((id) => id !== String(userId)); 70 | } 71 | } else if (value === "downVote") { 72 | if (upIndex !== -1) { 73 | question.upVote = question.upVote.filter((id) => id !== String(userId)); 74 | } 75 | if (downIndex === -1) { 76 | question.downVote.push(userId); 77 | } else { 78 | question.downVote = question.downVote.filter( 79 | (id) => id !== String(userId) 80 | ); 81 | } 82 | } 83 | await Questions.findByIdAndUpdate(_id, question); 84 | res.status(200).json({ message: "Voted successfully..." }); 85 | } catch (error) { 86 | res.status(404).json({ message: "Id not found" }); 87 | } 88 | }; -------------------------------------------------------------------------------- /server/controllers/users.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | import Users from '../models/auth.js' 3 | 4 | export const getAllUsers = async (req, res) => { 5 | try { 6 | const allUsers = await Users.find(); 7 | const allUserDetails = []; 8 | allUsers.forEach((user) => { 9 | allUserDetails.push({ 10 | _id: user._id, 11 | name: user.name, 12 | about: user.about, 13 | tags: user.tags, 14 | joinedOn: user.joinedOn, 15 | }); 16 | }); 17 | res.status(200).json(allUserDetails); 18 | } catch (error) { 19 | res.status(404).json({ message: error.message }); 20 | } 21 | }; 22 | 23 | export const updateProfile = async (req, res) => { 24 | const { id: _id} = req.params 25 | const { name, about, tags } = req.body 26 | if (!mongoose.Types.ObjectId.isValid(_id)) { 27 | return res.status(404).send("User unavailable..."); 28 | } 29 | try { 30 | const updatedProfile = await Users.findByIdAndUpdate( 31 | _id, 32 | { $set: { name: name, about : about, tags: tags }}, 33 | { new: true} 34 | ) 35 | res.status(200).json(updatedProfile); 36 | } catch (error) { 37 | res.status(405).json({ message: error.message }) 38 | } 39 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import dotenv from "dotenv"; 4 | import colors from 'colors' 5 | import userRoutes from "./routes/Users.js"; 6 | import questionRoutes from "./routes/Questions.js"; 7 | import answerRoutes from "./routes/Answers.js"; 8 | import chatbotRoutes from "./routes/Chatbot.js"; 9 | import connectDB from './config/connectDB.js' 10 | import otpRoutes from './routes/Otp.js' 11 | 12 | dotenv.config() 13 | connectDB() 14 | 15 | const PORT = process.env.PORT || 5000 16 | 17 | const app = express(); 18 | app.use(express.json({ limit: "30mb", extended: true })) 19 | app.use(express.urlencoded({ limit: "30mb", extended: true })) 20 | app.use(cors()) 21 | 22 | app.use("/user", userRoutes); 23 | app.use("/questions", questionRoutes); 24 | app.use("/answer", answerRoutes); 25 | app.use("/chatbot", chatbotRoutes) 26 | app.use('/otp', otpRoutes) 27 | 28 | 29 | app.get('/', (req, res) => { 30 | res.send("This is a stack overflow clone's API by Gautam Jha") 31 | }) 32 | 33 | app.listen(PORT, () => { 34 | console.log(`Server running on port ${PORT}`.bgBlue.white) 35 | }) 36 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | const auth = (req, res, next) => { 4 | try { 5 | const token = req.headers.authorization.split(" ")[1] 6 | 7 | let decodeData = jwt.verify(token, process.env.JWT_SECRET) 8 | req.userId = decodeData?.id; 9 | 10 | next(); 11 | } catch (error) { 12 | console.log(error) 13 | } 14 | }; 15 | 16 | export default auth; 17 | -------------------------------------------------------------------------------- /server/models/auth.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true 7 | }, 8 | email: { 9 | type: String, 10 | required: true 11 | }, 12 | password: { 13 | type: String, 14 | required: true 15 | }, 16 | about: { 17 | type: String, 18 | }, 19 | tags: { 20 | type: [String], 21 | }, 22 | joinedON: { 23 | type: Date, 24 | default: Date.now 25 | }, 26 | }) 27 | 28 | export default mongoose.model('Users', userSchema) -------------------------------------------------------------------------------- /server/models/otp.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | const Schema = mongoose.Schema 3 | 4 | const OTPSchema = new Schema({ 5 | name: String, 6 | email: { 7 | type: String, 8 | unique: true, 9 | }, 10 | otp: String, 11 | createdAt: Date, 12 | expiresAt: Date, 13 | }) 14 | 15 | const OTP = mongoose.model('OTP', OTPSchema) 16 | 17 | export default OTP -------------------------------------------------------------------------------- /server/models/questions.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const questionSchema = mongoose.Schema({ 4 | questionTitle: { 5 | type: String, 6 | required: "Question must have a title" 7 | }, 8 | questionBody: { 9 | type: String, 10 | required: "Question must have a body" 11 | }, 12 | questionTags: { 13 | type: [String], 14 | required: "Question must have tags" 15 | }, 16 | noOfAnswers: { 17 | type: Number, 18 | default: 0 19 | }, 20 | upVote: { 21 | type: [String], 22 | default: [] 23 | }, 24 | downVote: { 25 | type: [String], 26 | default: [] 27 | }, 28 | userPosted: { 29 | type: String, 30 | required: "Question must have an author" 31 | }, 32 | userId: { 33 | type: String 34 | }, 35 | askedOn: { 36 | type: Date, 37 | default: Date.now 38 | }, 39 | answer: [ 40 | { 41 | answerBody: String, 42 | userAnswered: String, 43 | userId: String, 44 | answeredOn: { 45 | type: Date, 46 | default: Date.now 47 | }, 48 | }, 49 | ], 50 | }) 51 | 52 | export default mongoose.model("Question", questionSchema); -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "stack-overflow-clone", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "nodemon index.js", 9 | "server": "node index.js" 10 | }, 11 | "author": "Gautam Jha", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.1.0", 15 | "bcryptjs": "^2.4.3", 16 | "colors": "^1.4.0", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.0.3", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.0", 21 | "mongoose": "^7.0.2", 22 | "nodemailer": "^6.9.1", 23 | "openai": "^3.2.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/routes/Answers.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { deleteAnswer, postAnswer } from '../controllers/answer.js' 4 | import auth from '../middleware/auth.js' 5 | 6 | const router = express.Router() 7 | 8 | router.patch('/post/:id', auth, postAnswer) 9 | router.patch('/delete/:id', auth, deleteAnswer) 10 | 11 | export default router -------------------------------------------------------------------------------- /server/routes/Chatbot.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { chatbotController } from '../controllers/chatbot.js' 3 | 4 | const router = express.Router() 5 | 6 | router.post('/', chatbotController) 7 | 8 | export default router -------------------------------------------------------------------------------- /server/routes/Otp.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { sendOTPController, verifyOTPController } from '../controllers/otp.js' 4 | 5 | const router = express.Router() 6 | 7 | router.post('/sendOTP', sendOTPController) 8 | router.post('/verifyOTP', verifyOTPController) 9 | 10 | export default router -------------------------------------------------------------------------------- /server/routes/Questions.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | import { askQuestion, deleteQuestion, getAllQuestions, voteQuestion } from '../controllers/questions.js' 4 | import auth from '../middleware/auth.js' 5 | 6 | const router = express.Router() 7 | 8 | router.post('/Ask', auth, askQuestion) 9 | router.get('/All', getAllQuestions) 10 | router.delete('/delete/:id', auth, deleteQuestion) 11 | router.patch("/vote/:id", auth, voteQuestion); 12 | 13 | export default router 14 | -------------------------------------------------------------------------------- /server/routes/Users.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import { logInController, signUpController } from '../controllers/auth.js' 3 | import { getAllUsers, updateProfile } from '../controllers/users.js' 4 | import auth from '../middleware/auth.js' 5 | 6 | const router = express.Router() 7 | 8 | router.post('/signup', signUpController) 9 | router.post('/login', logInController) 10 | 11 | router.get('/getAllUsers', getAllUsers) 12 | router.patch('/update/:id', auth, updateProfile) 13 | 14 | export default router -------------------------------------------------------------------------------- /server/utils/generateOTP.js: -------------------------------------------------------------------------------- 1 | const generateOTP = async () => { 2 | try { 3 | return `${Math.floor(100000 + Math.random() * 900000)}` 4 | } catch (error) { 5 | throw error; 6 | } 7 | } 8 | 9 | export default generateOTP -------------------------------------------------------------------------------- /server/utils/hashData.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | 3 | const hashData = async (data, salt = 10) => { 4 | try { 5 | const hashedData = await bcrypt.hash(data, salt) 6 | return hashedData; 7 | } catch (error) { 8 | throw Error 9 | } 10 | } 11 | 12 | const verifyHashedData = async (unhashed, hashed) => { 13 | try { 14 | const match = await bcrypt.compare(unhashed, hashed) 15 | return match 16 | } catch (error) { 17 | throw error 18 | } 19 | } 20 | 21 | export { hashData, verifyHashedData } -------------------------------------------------------------------------------- /server/utils/sendEmail.js: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer' 2 | 3 | const { AUTH_EMAIL, AUTH_PASS } = process.env 4 | 5 | let transporter = nodemailer.createTransport({ 6 | host: 'smtp-mail.outlook.com', 7 | auth: { 8 | user: AUTH_EMAIL, 9 | pass: AUTH_PASS, 10 | } 11 | }) 12 | 13 | // test transporter 14 | transporter.verify((error, success) => { 15 | if (error) { 16 | console.log(error) 17 | } else { 18 | console.log('Ready for messages') 19 | console.log(success) 20 | } 21 | }) 22 | 23 | const sendEmail = async (mailOptions) => { 24 | try { 25 | await transporter.sendMail(mailOptions) 26 | return; 27 | } catch (error) { 28 | throw(error) 29 | } 30 | } 31 | 32 | export default sendEmail 33 | --------------------------------------------------------------------------------