├── .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 |
115 |
118 | :
119 |
126 |
129 |
130 | }
131 | > : chatLog.length === 0 && Ask your queries!
132 | }
133 | {
134 | chatLog.map((message, index) => (
135 |
136 | ))
137 | }
138 |
139 |
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 |

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

21 |
Podcast 374: How valuable is your screen name?
22 |
23 |
24 |
Featured on Meta
25 |
26 |
27 |

28 |
Review queue workflows - Final release....
29 |
30 |
31 |

32 |
33 | Please welcome Valued Associates: #958 - V2Blast #959 - SpencerG
34 |
35 |
36 |
37 |

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 |
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 |

51 |
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 |
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 |
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 |
--------------------------------------------------------------------------------