├── backend ├── .gitignore ├── models │ ├── Comments.js │ ├── Question.js │ └── Answers.js ├── package.json ├── routers │ ├── index.js │ ├── Asnwer.js │ ├── Comments.js │ └── Question.js ├── db.js ├── server.js └── package-lock.json ├── frontend ├── src │ ├── App.css │ ├── components │ │ ├── StackOverflow │ │ │ ├── css │ │ │ │ ├── index.css │ │ │ │ ├── Sidebar.css │ │ │ │ ├── Main.css │ │ │ │ └── AllQuestions.css │ │ │ ├── index.js │ │ │ ├── Main.js │ │ │ ├── AllQuestions.js │ │ │ └── Sidebar.js │ │ ├── ViewQuestion │ │ │ ├── index.js │ │ │ ├── index.css │ │ │ └── MainQuestion.js │ │ ├── AddQuestion │ │ │ ├── TagsInput.js │ │ │ ├── index.css │ │ │ └── index.js │ │ ├── Auth │ │ │ ├── index.css │ │ │ └── index.js │ │ └── Header │ │ │ ├── css │ │ │ └── index.css │ │ │ └── index.js │ ├── app │ │ └── store.js │ ├── reportWebVitals.js │ ├── index.css │ ├── feature │ │ └── userSlice.js │ ├── firebase.js │ ├── utils │ │ └── Avatar.js │ ├── index.js │ └── App.js ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── .gitignore ├── package.json └── README.md ├── .gitignore └── package.json /backend/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akkySrivastava/stackoverflow-clone-mern/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akkySrivastava/stackoverflow-clone-mern/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akkySrivastava/stackoverflow-clone-mern/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/css/index.css: -------------------------------------------------------------------------------- 1 | .stack-index { 2 | min-width: fit-content; 3 | display: flex; 4 | 5 | min-height: 85vh; 6 | } 7 | 8 | .stack-index-content { 9 | display: flex; 10 | width: 100%; 11 | /* align-items: center; */ 12 | justify-content: center; 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/app/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from "@reduxjs/toolkit"; 2 | import userReducer from "../feature/userSlice"; 3 | // import threadReducer from "../features/counter/threadSlice"; 4 | 5 | export default configureStore({ 6 | reducer: { 7 | user: userReducer, 8 | // thread: threadReducer, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /frontend/.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.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /backend/models/Comments.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const CommentSchema = new mongoose.Schema({ 3 | // commentID: String, 4 | question_id: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "Questions", 7 | }, 8 | comment: String, 9 | created_at: { 10 | type: Date, 11 | default: Date.now(), 12 | }, 13 | user: Object, 14 | }); 15 | 16 | module.exports = mongoose.model("Comments", CommentSchema); 17 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /backend/models/Question.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const questionSchema = new mongoose.Schema({ 4 | title: String, 5 | body: String, 6 | tags: [], 7 | created_at: { 8 | type: Date, 9 | default: Date.now(), 10 | }, 11 | user: Object, 12 | comment_id: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: "Comments", 15 | }, 16 | }); 17 | 18 | module.exports = mongoose.model("Questions", questionSchema); 19 | -------------------------------------------------------------------------------- /frontend/src/components/ViewQuestion/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Sidebar from "../StackOverflow/Sidebar"; 3 | import "./index.css"; 4 | import MainQuestion from "./MainQuestion"; 5 | 6 | function Index() { 7 | return ( 8 |
9 |
10 | 11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default Index; 18 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /backend/models/Answers.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const answerSchema = new mongoose.Schema({ 4 | question_id: { 5 | type: mongoose.Schema.Types.ObjectId, 6 | ref: "Questions", 7 | }, 8 | answer: String, 9 | created_at: { 10 | type: Date, 11 | default: Date.now(), 12 | }, 13 | user: Object, 14 | comment_id: { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: "Comments", 17 | }, 18 | }); 19 | 20 | module.exports = mongoose.model("Answers", answerSchema); 21 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon server.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "body-parser": "^1.19.0", 15 | "cors": "^2.8.5", 16 | "dotenv": "^10.0.0", 17 | "express": "^4.17.1", 18 | "mongoose": "^5.13.6", 19 | "nodemon": "^2.0.12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/routers/index.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const express = require("express"); 3 | const router = express.Router(); 4 | const questionRouter = require("./Question"); 5 | const answerRouter = require("./Asnwer"); 6 | const commentRouter = require('./Comments') 7 | 8 | router.get("/", (req, res) => { 9 | res.send("Welcome to stack overflow clone"); 10 | }); 11 | 12 | router.use("/question", questionRouter); 13 | router.use("/answer", answerRouter); 14 | router.use('/comment', commentRouter) 15 | 16 | module.exports = router; 17 | -------------------------------------------------------------------------------- /frontend/src/feature/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | export const userSlice = createSlice({ 4 | name: "user", 5 | initialState: { 6 | value: 0, 7 | }, 8 | reducers: { 9 | login: (state, action) => { 10 | state.user = action.payload; 11 | }, 12 | logout: (state) => { 13 | state.user = null; 14 | }, 15 | }, 16 | }); 17 | 18 | export const { login, logout } = userSlice.actions; 19 | 20 | export const selectUser = (state) => state.user.user; 21 | 22 | export default userSlice.reducer; 23 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/routers/Asnwer.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | const answerDB = require("../models/Answers"); 4 | 5 | router.post("/", async (req, res) => { 6 | const answerData = new answerDB({ 7 | question_id: req.body.question_id, 8 | answer: req.body.answer, 9 | user: req.body.user, 10 | }); 11 | 12 | await answerData 13 | .save() 14 | .then((doc) => { 15 | res.status(201).send(doc); 16 | }) 17 | .catch((err) => { 18 | res.status(400).send({ 19 | message: "Answer not added successfully", 20 | }); 21 | }); 22 | }); 23 | 24 | module.exports = router; 25 | -------------------------------------------------------------------------------- /backend/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const url = 4 | "mongodb://akky:KdaWNLd6wxADLvy@cluster0-shard-00-00.rfatk.mongodb.net:27017,cluster0-shard-00-01.rfatk.mongodb.net:27017,cluster0-shard-00-02.rfatk.mongodb.net:27017/stackoverflow?ssl=true&replicaSet=atlas-i16i1b-shard-0&authSource=admin&retryWrites=true&w=majority"; 5 | module.exports.connect = () => { 6 | mongoose 7 | .connect(url, { 8 | useNewUrlParser: true, 9 | // useFindAndModify: false, 10 | useUnifiedTopology: true, 11 | // useCreateIndex: true, 12 | }) 13 | .then(() => console.log("MongoDB is connected successfully")) 14 | .catch((err) => console.log("Error: ", err)); 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | import { getAuth, GoogleAuthProvider } from "firebase/auth"; 3 | const firebaseConfig = { 4 | apiKey: "AIzaSyAm2j5QCRCCJlRX0r1qBG_be1bXdXSRdyY", 5 | authDomain: "stackoverflow-3f0d8.firebaseapp.com", 6 | projectId: "stackoverflow-3f0d8", 7 | storageBucket: "stackoverflow-3f0d8.appspot.com", 8 | messagingSenderId: "76298589116", 9 | appId: "1:76298589116:web:26ce6feaf0025dbdd511b9", 10 | measurementId: "G-LDJE2JW8YE", 11 | }; 12 | 13 | const firebaseApp = initializeApp(firebaseConfig); 14 | // const db = firebaseApp.firestore(); 15 | const auth = getAuth(); 16 | const provider = new GoogleAuthProvider(); 17 | 18 | export { auth, provider }; 19 | // export default db; 20 | -------------------------------------------------------------------------------- /frontend/src/utils/Avatar.js: -------------------------------------------------------------------------------- 1 | function stringToColor(string) { 2 | let hash = 0; 3 | let i; 4 | 5 | /* eslint-disable no-bitwise */ 6 | for (i = 0; i < string.length; i += 1) { 7 | hash = string.charCodeAt(i) + ((hash << 5) - hash); 8 | } 9 | 10 | let color = "#"; 11 | 12 | for (i = 0; i < 3; i += 1) { 13 | const value = (hash >> (i * 8)) & 0xff; 14 | color += `00${value.toString(16)}`.substr(-2); 15 | } 16 | /* eslint-enable no-bitwise */ 17 | 18 | return color; 19 | } 20 | 21 | export function stringAvatar(name) { 22 | return { 23 | sx: { 24 | bgcolor: name ? stringToColor(name) : "rgba(255,255,255,0.8)", 25 | }, 26 | children: name && `${name.split(" ")[0][0]}${name.split(" ")[1][0]}`, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/ViewQuestion/index.css: -------------------------------------------------------------------------------- 1 | .main-desc { 2 | display: flex; 3 | width: 100%; 4 | } 5 | 6 | .info { 7 | display: flex; 8 | align-items: center; 9 | font-size: small; 10 | } 11 | 12 | .info > p { 13 | color: rgba(0, 0, 0, 0.4); 14 | margin: 0 10px; 15 | } 16 | 17 | .info > p > span { 18 | color: rgba(0, 0, 0, 0.8); 19 | margin: 0 5px; 20 | } 21 | 22 | .arrow { 23 | font-size: 2rem; 24 | color: rgba(0, 0, 0, 0.25); 25 | } 26 | 27 | .all-options > .MuiSvgIcon-root { 28 | color: rgba(0, 0, 0, 0.25); 29 | font-size: large; 30 | margin: 5px 0; 31 | } 32 | 33 | /* .ql-syntax { 34 | display: flex; 35 | width: 90%; 36 | overflow: hidden; 37 | } */ 38 | pre { 39 | display: flex; 40 | width: 90px; 41 | flex-wrap: wrap; 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Sidebar from "./Sidebar"; 3 | import "./css/index.css"; 4 | import Main from "./Main"; 5 | import axios from "axios"; 6 | 7 | function Index() { 8 | const [questions, setQuestions] = useState([]); 9 | 10 | useEffect(() => { 11 | async function getQuestion() { 12 | await axios.get("/api/question").then((res) => { 13 | setQuestions(res.data.reverse()); 14 | // console.log(res.data) 15 | }); 16 | } 17 | getQuestion(); 18 | }, []); 19 | return ( 20 |
21 |
22 | 23 |
24 |
25 |
26 | ); 27 | } 28 | 29 | export default Index; 30 | -------------------------------------------------------------------------------- /backend/routers/Comments.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const router = express.Router(); 3 | 4 | const commentDB = require("../models/Comments"); 5 | 6 | router.post("/:id", async (req, res) => { 7 | try { 8 | await commentDB 9 | .create({ 10 | question_id: req.params.id, 11 | comment: req.body.comment, 12 | user: req.body.user, 13 | }) 14 | .then((doc) => { 15 | res.status(201).send({ 16 | message: "Comment added successfully", 17 | }); 18 | }) 19 | .catch((err) => { 20 | res.status(400).send({ 21 | message: "Bad format", 22 | }); 23 | }); 24 | } catch (err) { 25 | res.status(500).send({ 26 | message: "Error while adding comments", 27 | }); 28 | } 29 | }); 30 | 31 | module.exports = router; 32 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import hljs from "highlight.js"; 7 | import "highlight.js/styles/github.css"; 8 | import store from "./app/store"; 9 | import { Provider } from "react-redux"; 10 | 11 | hljs.configure({ 12 | // optionally configure hljs 13 | languages: [ 14 | "javascript", 15 | "ruby", 16 | "python", 17 | "c", 18 | "c++", 19 | "java", 20 | "HTML", 21 | "css", 22 | "perl", 23 | "R", 24 | "matlab", 25 | ], 26 | }); 27 | ReactDOM.render( 28 | 29 | 30 | 31 | 32 | , 33 | document.getElementById("root") 34 | ); 35 | 36 | // If you want to start measuring performance in your app, pass a function 37 | // to log results (for example: reportWebVitals(console.log)) 38 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 39 | reportWebVitals(); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stackoverflow-clone", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon --watch backend --exec node backend/server.js", 9 | "server": "nodemon backend/server.js", 10 | "client": "npm start --prefix frontend", 11 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix frontend && npm run build --prefix frontend" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/akkySrivastava/stackoverflow-clone-mern.git" 16 | }, 17 | "author": "Code with akky", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/akkySrivastava/stackoverflow-clone-mern/issues" 21 | }, 22 | "homepage": "https://github.com/akkySrivastava/stackoverflow-clone-mern#readme", 23 | "dependencies": { 24 | "axios": "^0.21.1", 25 | "body-parser": "^1.19.0", 26 | "cors": "^2.8.5", 27 | "dotenv": "^10.0.0", 28 | "express": "^4.17.1", 29 | "mongoose": "^6.0.2", 30 | "nodemon": "^2.0.12" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== "production") { 2 | require("dotenv").config(); 3 | } 4 | 5 | const express = require("express"); 6 | const cors = require("cors"); 7 | const path = require("path"); 8 | const app = express(); 9 | const router = require("./routers"); 10 | const bodyParser = require("body-parser"); 11 | const PORT = process.env.PORT || 80; 12 | 13 | const db = require("./db"); 14 | db.connect(); 15 | 16 | app.use(bodyParser.json({ limit: "500mb" })); 17 | app.use(bodyParser.urlencoded({ extended: true, limit: "500mb" })); 18 | 19 | app.use(express.json()); 20 | app.use((req, res, next) => { 21 | res.header("Access-Control-Allow-Origin", "*"); 22 | res.header("Access-Control-Allow-Headers", "*"); 23 | next(); 24 | }); 25 | 26 | app.use("/api", router); 27 | app.use("/uploads", express.static(path.join(__dirname, "/../uploads"))); 28 | app.use(express.static(path.join(__dirname, "/../frontend/build"))); 29 | 30 | app.get("*", (req, res) => { 31 | try { 32 | res.sendFile(path.join(`${__dirname}/../frontend/build/index.html`)); 33 | } catch (e) { 34 | res.send("Welcome to stackoverflow clone"); 35 | } 36 | }); 37 | 38 | app.use(cors()); 39 | 40 | app.listen(PORT, () => { 41 | console.log(`Stack Overflow Clone API is running on PORT No- ${PORT}`); 42 | }); 43 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/css/Sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | display: flex; 3 | padding: 20px 0px 10px 10px; 4 | height: 100%; 5 | border-right: 1px solid #ddd; 6 | flex: 0.5; 7 | max-width: 200px; 8 | } 9 | 10 | .sidebar-container { 11 | margin: 10px 0; 12 | display: flex; 13 | width: 100%; 14 | } 15 | 16 | .sidebar-options { 17 | display: flex; 18 | flex-direction: column; 19 | width: 200px; 20 | } 21 | 22 | .sidebar-option { 23 | display: flex; 24 | flex-direction: column; 25 | margin: 10px 0; 26 | font-size: 14px; 27 | } 28 | 29 | a { 30 | text-decoration: none; 31 | color: rgba(0, 0, 0, 0.534); 32 | } 33 | 34 | a:hover { 35 | color: #000; 36 | } 37 | 38 | .sidebar-option > p { 39 | color: rgba(0, 0, 0, 0.534); 40 | font-size: 14px; 41 | } 42 | 43 | .link { 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | .link-tag { 49 | display: flex; 50 | align-items: center; 51 | padding: 5px 0; 52 | width: 100%; 53 | } 54 | 55 | .link-tag:hover { 56 | border-right: 5px solid rgb(245, 162, 9); 57 | } 58 | 59 | .link-tag > .MuiSvgIcon-root { 60 | font-size: 18px; 61 | margin-right: 5px; 62 | color: rgb(245, 162, 9); 63 | } 64 | 65 | .tags { 66 | display: flex; 67 | flex-direction: column; 68 | color: rgba(0, 0, 0, 0.534); 69 | margin: 0px 20px; 70 | } 71 | 72 | .tags > p { 73 | margin: 5px 0; 74 | cursor: pointer; 75 | } 76 | 77 | .tags > p:hover { 78 | color: #000; 79 | } 80 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stackoverflow-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://127.0.0.1:80", 6 | "dependencies": { 7 | "@material-ui/core": "^4.12.3", 8 | "@material-ui/icons": "^4.11.2", 9 | "@mui/material": "^5.2.4", 10 | "@reduxjs/toolkit": "^1.7.0", 11 | "@testing-library/jest-dom": "^5.14.1", 12 | "@testing-library/react": "^11.2.7", 13 | "@testing-library/user-event": "^12.8.3", 14 | "bootstrap": "^5.1.0", 15 | "firebase": "^9.6.1", 16 | "highlight.js": "^11.2.0", 17 | "react": "^17.0.2", 18 | "react-dom": "^17.0.2", 19 | "react-html-parser": "^2.0.2", 20 | "react-quill": "^1.3.5", 21 | "react-redux": "^7.2.6", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "4.0.3", 24 | "react-tag-input-component": "^1.0.7", 25 | "web-vitals": "^1.1.2" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/components/AddQuestion/TagsInput.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { makeStyles } from "@material-ui/core/styles"; 3 | import Chip from "@material-ui/core/Chip"; 4 | import Paper from "@material-ui/core/Paper"; 5 | import TagFacesIcon from "@material-ui/icons/TagFaces"; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | root: { 9 | display: "flex", 10 | justifyContent: "center", 11 | flexWrap: "wrap", 12 | listStyle: "none", 13 | padding: theme.spacing(0.5), 14 | margin: 0, 15 | }, 16 | chip: { 17 | margin: theme.spacing(0.5), 18 | }, 19 | })); 20 | 21 | export default function ChipsArray() { 22 | const classes = useStyles(); 23 | const [chipData, setChipData] = React.useState([ 24 | { key: 0, label: "Angular" }, 25 | { key: 1, label: "jQuery" }, 26 | { key: 2, label: "Polymer" }, 27 | { key: 3, label: "React" }, 28 | { key: 4, label: "Vue.js" }, 29 | ]); 30 | 31 | const handleDelete = (chipToDelete) => () => { 32 | setChipData((chips) => 33 | chips.filter((chip) => chip.key !== chipToDelete.key) 34 | ); 35 | }; 36 | 37 | return ( 38 | 39 | {chipData.map((data) => { 40 | let icon; 41 | 42 | if (data.label === "React") { 43 | icon = ; 44 | } 45 | 46 | return ( 47 |
  • 48 | 54 |
  • 55 | ); 56 | })} 57 |
    58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/components/Auth/index.css: -------------------------------------------------------------------------------- 1 | .auth { 2 | display: flex; 3 | flex-direction: column; 4 | /* width: 100vw; */ 5 | height: 90vh; 6 | padding: 30px 0; 7 | background-color: #eee; 8 | } 9 | 10 | .auth-container { 11 | display: flex; 12 | flex-direction: column; 13 | width: 100%; 14 | align-items: center; 15 | justify-content: center; 16 | } 17 | 18 | .auth-container > p { 19 | margin-bottom: 30px; 20 | font-size: 1.5rem; 21 | } 22 | 23 | .sign-options { 24 | display: flex; 25 | flex-direction: column; 26 | width: 300px; 27 | } 28 | 29 | .single-option { 30 | display: flex; 31 | padding: 9px; 32 | margin: 5px 0; 33 | border-radius: 3px; 34 | color: rgba(0, 0, 0, 0.8); 35 | background-color: #fff; 36 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 37 | 0 6px 20px 0 rgba(66, 42, 42, 0.19); 38 | cursor: pointer; 39 | transition: all 0.3s; 40 | } 41 | 42 | .single-option:hover { 43 | box-shadow: 0 4px 8px 0 #0095ff15, 0 6px 20px 0 #0095ff52; 44 | } 45 | 46 | .single-option > p { 47 | margin-left: 10px; 48 | } 49 | img { 50 | width: 20px; 51 | } 52 | 53 | .auth-login { 54 | display: flex; 55 | margin: 40px 0; 56 | width: 300px; 57 | } 58 | 59 | .auth-login-container { 60 | width: 100%; 61 | display: flex; 62 | flex-direction: column; 63 | padding: 20px; 64 | background-color: #fff; 65 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 66 | 0 6px 20px 0 rgba(66, 42, 42, 0.19); 67 | border-radius: 3px; 68 | } 69 | 70 | .input-field { 71 | display: flex; 72 | flex-direction: column; 73 | } 74 | 75 | .input-field > p { 76 | font-size: 1.1rem; 77 | margin: 10px 0; 78 | } 79 | 80 | .input-field > input { 81 | padding: 10px; 82 | border: 1px solid #0095ff8e; 83 | background-color: transparent; 84 | border-radius: 3px; 85 | outline: none; 86 | } 87 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FilterListIcon from "@material-ui/icons/FilterList"; 3 | import "./css/Main.css"; 4 | import AllQuestions from "./AllQuestions"; 5 | import { Link } from "react-router-dom"; 6 | // import axios from "axios"; 7 | 8 | function Main({ questions }) { 9 | // const [questions, setQuestions] = useState([]); 10 | 11 | // console.log(questions); 12 | return ( 13 |
    14 |
    15 |
    16 |

    All Questions

    17 | 18 | 19 | 20 | 21 | {/* */} 22 | 23 | {/* */} 24 |
    25 |
    26 |

    {questions.length} questions

    27 |
    28 |
    29 |
    30 | {/* Newest */} 31 | Newest 32 |
    33 |
    34 | Active 35 | 36 | {/* Active */} 37 |
    38 |
    39 | {/* More */} 40 | More 41 |
    42 |
    43 |
    44 | 45 |

    Filter

    46 |
    47 |
    48 |
    49 |
    50 | {questions?.map((_q) => ( 51 |
    52 | 53 |
    54 | ))} 55 |
    56 |
    57 |
    58 | ); 59 | } 60 | 61 | export default Main; 62 | -------------------------------------------------------------------------------- /frontend/src/components/AddQuestion/index.css: -------------------------------------------------------------------------------- 1 | .add-question { 2 | display: flex; 3 | width: 100%; 4 | background-color: rgba(238, 238, 238, 0.568); 5 | 6 | height: 100vh; 7 | justify-content: center; 8 | } 9 | 10 | .add-question-container { 11 | padding: 30px 15px; 12 | display: flex; 13 | flex-direction: column; 14 | width: 95%; 15 | max-width: 800px; 16 | } 17 | 18 | .head-title { 19 | display: flex; 20 | width: 100%; 21 | } 22 | 23 | .head-title > h1 { 24 | margin-bottom: 20px; 25 | font-weight: 400; 26 | font-size: 26px; 27 | } 28 | 29 | .question-container { 30 | display: flex; 31 | padding: 15px; 32 | background-color: #fff; 33 | border: 1px solid #eee; 34 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 35 | 0 6px 20px 0 rgba(66, 42, 42, 0.19); 36 | } 37 | 38 | .question-options { 39 | display: flex; 40 | flex-direction: column; 41 | width: 100%; 42 | } 43 | 44 | .question-option { 45 | /* display: flex; */ 46 | flex-direction: column; 47 | width: 100%; 48 | } 49 | 50 | .title { 51 | display: flex; 52 | flex-direction: column; 53 | margin: 10px 0px; 54 | font-size: 0.9rem; 55 | } 56 | 57 | .title > h3 { 58 | color: rgba(0, 0, 0, 0.8); 59 | font-weight: 500; 60 | } 61 | 62 | .title > small { 63 | color: rgba(0, 0, 0, 0.8); 64 | } 65 | 66 | .title > input { 67 | margin: 5px 0px; 68 | padding: 10px; 69 | border: 1px solid rgba(0, 0, 0, 0.2); 70 | border-radius: 3px; 71 | outline: none; 72 | } 73 | 74 | .title > input::placeholder { 75 | color: #ddd; 76 | } 77 | .title > input:focus { 78 | border: 1px solid #0054ff; 79 | box-shadow: 0 4px 8px 0 #0055ff23, 0 6px 20px 0 #0055ff11; 80 | } 81 | 82 | .quill { 83 | height: 100%; 84 | } 85 | 86 | .react-quill { 87 | margin: 10px 0; 88 | border-radius: 10px; 89 | } 90 | 91 | .ql-editor { 92 | height: 200px; 93 | } 94 | 95 | .button { 96 | max-width: fit-content; 97 | margin: 10px 0px; 98 | } 99 | 100 | button:hover { 101 | background-color: #053086; 102 | } 103 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { 3 | BrowserRouter as Router, 4 | Switch, 5 | Route, 6 | Redirect, 7 | } from "react-router-dom"; 8 | import StackOverflow from "./components/StackOverflow"; 9 | import Header from "./components/Header"; 10 | import AddQuestion from "./components/AddQuestion"; 11 | import ViewQuestion from "./components/ViewQuestion"; 12 | import Auth from "./components/Auth"; 13 | import { useDispatch, useSelector } from "react-redux"; 14 | import { login, logout, selectUser } from "./feature/userSlice"; 15 | import { useEffect } from "react"; 16 | import { auth } from "./firebase"; 17 | 18 | function App() { 19 | const user = useSelector(selectUser); 20 | const dispatch = useDispatch(); 21 | 22 | useEffect(() => { 23 | auth.onAuthStateChanged((authUser) => { 24 | if (authUser) { 25 | dispatch( 26 | login({ 27 | uid: authUser.uid, 28 | photo: authUser.photoURL, 29 | displayName: authUser.displayName, 30 | email: authUser.email, 31 | }) 32 | ); 33 | } else { 34 | dispatch(logout()); 35 | } 36 | // console.log(authUser); 37 | }); 38 | }, [dispatch]); 39 | 40 | const PrivateRoute = ({ component: Component, ...rest }) => ( 41 | 44 | user ? ( 45 | 46 | ) : ( 47 | 55 | ) 56 | } 57 | /> 58 | ); 59 | 60 | return ( 61 |
    62 | 63 |
    64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
    72 | ); 73 | } 74 | 75 | export default App; 76 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/css/Main.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | /* max-width: 700px; */ 4 | padding: 30px 10px; 5 | flex: 0.75; 6 | flex-direction: column; 7 | } 8 | 9 | .main-container { 10 | display: flex; 11 | flex-direction: column; 12 | width: 100%; 13 | } 14 | 15 | .main-top { 16 | display: flex; 17 | width: 100%; 18 | align-items: center; 19 | justify-content: space-between; 20 | margin-bottom: 10px; 21 | } 22 | 23 | .main-top > h2 { 24 | font-weight: 400; 25 | font-size: 25px; 26 | color: rgba(0, 0, 0, 0.8); 27 | } 28 | 29 | button { 30 | padding: 10px; 31 | background-color: #0095ff; 32 | color: #fff; 33 | border: 2px solid #007cd446; 34 | outline: none; 35 | border-radius: 3px; 36 | cursor: pointer; 37 | } 38 | 39 | .main-desc { 40 | display: flex; 41 | flex-direction: row; 42 | align-items: center; 43 | font-size: 1.1rem; 44 | color: rgba(0, 0, 0, 0.8); 45 | justify-content: space-between; 46 | padding: 0px 0px 10px 0px; 47 | border-bottom: 1px solid #ddd; 48 | margin-top: 10px; 49 | } 50 | 51 | .main-filter { 52 | display: flex; 53 | align-items: center; 54 | } 55 | 56 | .main-tabs { 57 | display: flex; 58 | border: 1px solid rgba(0, 0, 0, 0.4); 59 | margin-right: 20px; 60 | border-radius: 3px; 61 | } 62 | 63 | .main-tab { 64 | padding: 5px; 65 | border-right: 1px solid rgba(0, 0, 0, 0.4); 66 | } 67 | .main-tab > a { 68 | font-size: small; 69 | } 70 | 71 | .main-filter-item { 72 | display: flex; 73 | padding: 5px; 74 | border: 1px solid #0095ff; 75 | border-radius: 3px; 76 | background-color: #00ccff17; 77 | font-size: small; 78 | align-items: center; 79 | color: #007cd4; 80 | cursor: pointer; 81 | } 82 | 83 | .questions { 84 | display: flex; 85 | flex-direction: column; 86 | width: 100%; 87 | } 88 | 89 | .question { 90 | display: flex; 91 | flex-direction: column; 92 | padding: 15px 0px; 93 | border-bottom: 1px solid #eee; 94 | width: 100%; 95 | } 96 | 97 | pre { 98 | display: flex; 99 | width: 90px; 100 | flex-wrap: wrap; 101 | } 102 | 103 | @media screen and (min-width: 768px) { 104 | .main { 105 | /* display: flex; 106 | max-width: 700px; 107 | padding: 30px 10px; */ 108 | flex: 0.6; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/css/AllQuestions.css: -------------------------------------------------------------------------------- 1 | .all-questions { 2 | display: flex; 3 | /* flex-direction: column; */ 4 | width: 100%; 5 | padding: 20px 0px; 6 | border-bottom: 1px solid #eee; 7 | width: 100%; 8 | } 9 | 10 | .all-questions-container { 11 | display: flex; 12 | flex-direction: column; 13 | flex-direction: row; 14 | /* align-items: center; */ 15 | justify-content: space-between; 16 | width: 100%; 17 | } 18 | 19 | .all-questions-left { 20 | display: flex; 21 | margin-right: 30px; 22 | } 23 | 24 | .all-options { 25 | display: flex; 26 | flex-direction: column; 27 | align-items: center; 28 | color: rgba(0, 0, 0, 0.7); 29 | font-size: small; 30 | } 31 | 32 | .all-option { 33 | display: flex; 34 | flex-direction: column; 35 | align-items: center; 36 | margin-bottom: 10px; 37 | } 38 | 39 | .all-option > p { 40 | font-size: large; 41 | } 42 | 43 | .question-answer { 44 | display: flex; 45 | flex-direction: column; 46 | width: 100%; 47 | } 48 | 49 | .question-answer > a { 50 | color: #0151f0d8; 51 | font-size: 1.1rem; 52 | margin-bottom: 10px; 53 | } 54 | .question-answer > a:hover { 55 | color: #1649b1d8; 56 | font-size: 1.1rem; 57 | margin-bottom: 10px; 58 | } 59 | 60 | .question-answer > p { 61 | font-size: 0.9rem; 62 | color: rgba(0, 0, 0, 0.7); 63 | margin-bottom: 10px; 64 | } 65 | 66 | .author { 67 | display: flex; 68 | flex-direction: column; 69 | margin-left: auto; 70 | } 71 | 72 | .author > small { 73 | color: rgba(0, 0, 0, 0.5); 74 | margin-bottom: 5px; 75 | } 76 | 77 | .auth-details { 78 | display: flex; 79 | align-items: center; 80 | } 81 | 82 | .auth-details > p { 83 | margin-left: 5px; 84 | font-size: small; 85 | color: #0151f0d8; 86 | } 87 | 88 | .comments { 89 | margin: 10px 0; 90 | width: 90%; 91 | margin-left: auto; 92 | } 93 | 94 | .comment { 95 | display: flex; 96 | flex-direction: column; 97 | font-size: 0.9rem; 98 | padding: 10px 0px; 99 | border-bottom: 1px solid #eee; 100 | border-top: 1px solid #eee; 101 | } 102 | 103 | .comment > p { 104 | margin: 10px 0; 105 | } 106 | 107 | .comment > p > span { 108 | padding: 3px; 109 | background-color: #0151f028; 110 | color: #0151f0d8; 111 | border-radius: 2px; 112 | } 113 | 114 | .comments > p { 115 | margin-left: -30px; 116 | margin-top: 20px; 117 | font-size: small; 118 | cursor: pointer; 119 | color: rgba(0, 0, 0, 0.4); 120 | } 121 | 122 | .comments > p:hover { 123 | color: #0151f0a2; 124 | } 125 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/AllQuestions.js: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@material-ui/core"; 2 | import React, { useState } from "react"; 3 | import "./css/AllQuestions.css"; 4 | import ReactHtmlParser from "react-html-parser"; 5 | import { Link } from "react-router-dom"; 6 | import { stringAvatar } from "../../utils/Avatar"; 7 | 8 | function AllQuestions({ data }) { 9 | function truncate(str, n) { 10 | return str?.length > n ? str.substr(0, n - 1) + "..." : str; 11 | } 12 | 13 | let tags = JSON.parse(data?.tags[0]); 14 | // console.log(); 15 | return ( 16 |
    17 |
    18 |
    19 |
    20 |
    21 |

    0

    22 | votes 23 |
    24 |
    25 |

    {data?.answerDetails?.length}

    26 | answers 27 |
    28 |
    29 | 2 views 30 |
    31 |
    32 |
    33 |
    34 | {data.title} 35 | 36 | {/* {data.title} */} 37 | 38 |
    43 |
    {ReactHtmlParser(truncate(data.body, 200))}
    44 |
    45 |
    50 | {tags.map((_tag) => ( 51 |

    59 | {_tag} 60 |

    61 | ))} 62 |
    63 |
    64 | {data.create_at} 65 |
    66 | 67 |

    68 | {data?.user?.displayName 69 | ? data?.user?.displayName 70 | : "Natalie lee"} 71 |

    72 |
    73 |
    74 |
    75 |
    76 |
    77 | ); 78 | } 79 | 80 | export default AllQuestions; 81 | -------------------------------------------------------------------------------- /frontend/src/components/Header/css/index.css: -------------------------------------------------------------------------------- 1 | header { 2 | /* width: 100vw; */ 3 | min-width: fit-content; 4 | display: flex; 5 | position: sticky; 6 | top: 0px; 7 | z-index: 1000; 8 | min-height: 5vh; 9 | } 10 | .header-container { 11 | display: flex; 12 | flex-direction: row; 13 | width: 100%; 14 | align-items: center; 15 | justify-content: space-around; 16 | /* padding: 10px 10px; */ 17 | background-color: rgb(251, 250, 250); 18 | 19 | box-shadow: 0px 0.5px 8px rgba(0, 0, 0, 0.178); 20 | } 21 | 22 | .header-left { 23 | display: flex; 24 | flex-direction: row; 25 | margin: 0px 10px; 26 | align-items: center; 27 | } 28 | 29 | .header-left > a > img { 30 | width: 150px; 31 | object-fit: contain; 32 | padding: 0px 0px; 33 | } 34 | 35 | .header-left > a > img:hover { 36 | background-color: #ddd; 37 | cursor: pointer; 38 | padding: 10px 0px; 39 | } 40 | 41 | .header-left > h3 { 42 | font-weight: 400; 43 | font-size: 14px; 44 | margin: 0px 10px; 45 | padding: 5px 10px; 46 | cursor: pointer; 47 | } 48 | 49 | .header-left > h3:hover { 50 | background-color: #ddd; 51 | border-radius: 33px; 52 | } 53 | 54 | .header-middle { 55 | display: flex; 56 | flex-direction: row; 57 | align-items: center; 58 | /* padding: 0px 10px; */ 59 | } 60 | 61 | .header-middle > .header-search-container { 62 | display: flex; 63 | padding: 10px 10px; 64 | margin-right: 10px; 65 | background-color: #fff; 66 | border-radius: 3px; 67 | border: 1px solid rgba(34, 34, 34, 0.233); 68 | } 69 | 70 | .header-search-container > input { 71 | border: none; 72 | width: 100%; 73 | margin-left: 5px; 74 | outline: none; 75 | } 76 | 77 | .header-search-container > .MuiSvgIcon-root { 78 | color: #ccc; 79 | } 80 | 81 | .header-right { 82 | display: flex; 83 | } 84 | 85 | .header-right > .header-right-container { 86 | display: flex; 87 | align-items: center; 88 | padding: 5px 10px; 89 | } 90 | 91 | .header-right-container > .MuiSvgIcon-root { 92 | margin: 0px 5px; 93 | color: rgba(0, 0, 0, 0.534); 94 | padding: 10px 5px; 95 | cursor: pointer; 96 | } 97 | 98 | .header-right-container > .MuiSvgIcon-root:hover { 99 | background-color: #ddd; 100 | color: #000; 101 | } 102 | 103 | .header-right-container > img { 104 | width: 20px; 105 | margin: 0 10px; 106 | cursor: pointer; 107 | } 108 | 109 | @media screen and (max-width: 768px) { 110 | .header-middle { 111 | display: none; 112 | } 113 | } 114 | 115 | @media screen and (min-width: 768px) { 116 | .header-middle { 117 | width: calc(20 * 2vw); 118 | } 119 | 120 | .header-search-container { 121 | width: 100%; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /frontend/src/components/StackOverflow/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PublicIcon from "@material-ui/icons/Public"; 3 | import StarsIcon from "@material-ui/icons/Stars"; 4 | import WorkIcon from "@material-ui/icons/Work"; 5 | import "./css/Sidebar.css"; 6 | import { Link } from "react-router-dom"; 7 | 8 | function Sidebar() { 9 | return ( 10 |
    11 |
    12 |
    13 |
    14 | Home 15 | 16 | {/* Home */} 17 |
    18 |
    19 |

    PUBLIC

    20 |
    21 |
    22 | 23 | Question 24 | 25 | {/* Question */} 26 |
    27 | 28 |
    29 |

    Tags

    30 |

    Users

    31 |
    32 |
    33 |
    34 |
    35 |

    COLLECTIVES

    36 |
    37 |
    38 | 39 | Explore Collectives 40 | 41 | {/* Explore Collectives */} 42 |
    43 |
    44 |
    45 |
    46 |

    FIND A JOB

    47 |
    48 | 54 | Jobs 55 | 56 | {/* 62 | Jobs 63 | */} 64 | {/* 70 | Companies 71 | */} 72 | 78 | Companies 79 | 80 |
    81 |
    82 |
    83 |

    TEAMS

    84 |
    85 | 86 | Companies 87 | {/* Companies */} 88 |
    89 |
    90 |
    91 |
    92 |
    93 | ); 94 | } 95 | 96 | export default Sidebar; 97 | -------------------------------------------------------------------------------- /frontend/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./css/index.css"; 3 | import SearchIcon from "@material-ui/icons/Search"; 4 | import { Avatar } from "@material-ui/core"; 5 | // import Avatar from "@mui/material/Avatar"; 6 | import InboxIcon from "@material-ui/icons/Inbox"; 7 | import HelpIcon from "@material-ui/icons/Help"; 8 | import { Link } from "react-router-dom"; 9 | import { auth } from "../../firebase"; 10 | import { useSelector } from "react-redux"; 11 | import { selectUser } from "../../feature/userSlice"; 12 | 13 | function Header() { 14 | const user = useSelector(selectUser); 15 | // console.log(user); 16 | function stringToColor(string) { 17 | let hash = 0; 18 | let i; 19 | 20 | /* eslint-disable no-bitwise */ 21 | for (i = 0; i < string.length; i += 1) { 22 | hash = string.charCodeAt(i) + ((hash << 5) - hash); 23 | } 24 | 25 | let color = "#"; 26 | 27 | for (i = 0; i < 3; i += 1) { 28 | const value = (hash >> (i * 8)) & 0xff; 29 | color += `00${value.toString(16)}`.substr(-2); 30 | } 31 | /* eslint-enable no-bitwise */ 32 | 33 | return color; 34 | } 35 | 36 | function stringAvatar(name) { 37 | return { 38 | sx: { 39 | bgcolor: name ? stringToColor(name) : "rgba(255,255,255,0.8)", 40 | }, 41 | children: name && `${name.split(" ")[0][0]}${name.split(" ")[1][0]}`, 42 | }; 43 | } 44 | return ( 45 |
    46 |
    47 |
    48 | 49 | logo 53 | 54 | {/* 55 | 56 | */} 57 | 58 |

    Products

    59 |
    60 |
    61 |
    62 | 63 | 64 |
    65 |
    66 |
    67 |
    68 | {window.innerWidth < 768 && } 69 | 70 | auth.signOut()} 76 | /> 77 | 78 | 79 | 92 | {/* stack-exchange */} 96 |
    97 |
    98 |
    99 |
    100 | ); 101 | } 102 | 103 | export default Header; 104 | -------------------------------------------------------------------------------- /frontend/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 the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will 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 | -------------------------------------------------------------------------------- /frontend/src/components/AddQuestion/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useSelector } from "react-redux"; 3 | import ReactQuill from "react-quill"; 4 | import "react-quill/dist/quill.snow.css"; // ES6 5 | import "./index.css"; 6 | import Editor from "react-quill/lib/toolbar"; 7 | import axios from "axios"; 8 | import { TagsInput } from "react-tag-input-component"; 9 | import { selectUser } from "../../feature/userSlice"; 10 | import { useHistory } from "react-router-dom"; 11 | // import ChipsArray from "./TagsInput"; 12 | 13 | function Index() { 14 | const user = useSelector(selectUser); 15 | var toolbarOptions = [ 16 | ["bold", "italic", "underline", "strike"], // toggled buttons 17 | ["blockquote", "code-block"], 18 | 19 | [{ header: 1 }, { header: 2 }], // custom button values 20 | [{ list: "ordered" }, { list: "bullet" }], 21 | [{ script: "sub" }, { script: "super" }], // superscript/subscript 22 | [{ indent: "-1" }, { indent: "+1" }], // outdent/indent 23 | [{ direction: "rtl" }], // text direction 24 | 25 | [{ size: ["small", false, "large", "huge"] }], // custom dropdown 26 | [{ header: [1, 2, 3, 4, 5, 6, false] }], 27 | 28 | [{ color: [] }, { background: [] }], // dropdown with defaults from theme 29 | [{ font: [] }], 30 | [{ align: [] }], 31 | 32 | ["clean"], // remove formatting button 33 | ]; 34 | Editor.modules = { 35 | syntax: false, 36 | toolbar: toolbarOptions, 37 | clipboard: { 38 | // toggle to add extra line breaks when pasting HTML: 39 | matchVisual: false, 40 | }, 41 | }; 42 | /* 43 | * Quill editor formats 44 | * See https://quilljs.com/docs/formats/ 45 | */ 46 | Editor.formats = [ 47 | "header", 48 | "font", 49 | "size", 50 | "bold", 51 | "italic", 52 | "underline", 53 | "strike", 54 | "blockquote", 55 | "list", 56 | "bullet", 57 | "indent", 58 | "link", 59 | "image", 60 | "video", 61 | ]; 62 | 63 | /* 64 | * PropType validation 65 | */ 66 | 67 | const [title, setTitle] = useState(""); 68 | const [body, setBody] = useState(""); 69 | const [tag, setTag] = useState([]); 70 | const history = useHistory(); 71 | 72 | const handleQuill = (value) => { 73 | setBody(value); 74 | }; 75 | 76 | const handleSubmit = async (e) => { 77 | e.preventDefault(); 78 | 79 | if (title !== "" && body !== "") { 80 | const bodyJSON = { 81 | title: title, 82 | body: body, 83 | tag: JSON.stringify(tag), 84 | user: user, 85 | }; 86 | await axios 87 | .post("/api/question", bodyJSON) 88 | .then((res) => { 89 | // console.log(res.data); 90 | alert("Question added successfully"); 91 | history.push("/"); 92 | }) 93 | .catch((err) => { 94 | console.log(err); 95 | }); 96 | } 97 | }; 98 | return ( 99 |
    100 |
    101 |
    102 |

    Ask a public question

    103 |
    104 |
    105 |
    106 |
    107 |
    108 |

    Title

    109 | 110 | Be specific and imagine you’re asking a question to another 111 | person 112 | 113 | setTitle(e.target.value)} 116 | type="text" 117 | placeholder="e.g Is there an R function for finding teh index of an element in a vector?" 118 | /> 119 |
    120 |
    121 |
    122 |
    123 |

    Body

    124 | 125 | Include all the information someone would need to answer your 126 | question 127 | 128 | 135 |
    136 |
    137 |
    138 |
    139 |

    Tags

    140 | 141 | Add up to 5 tags to describe what your question is about 142 | 143 | {/* setTag(e.target.value)} 146 | data-role="tagsinput" 147 | data-tag-trigger="Space" 148 | type="text" 149 | placeholder="e.g. (asp.net-mvc php react json)" 150 | /> */} 151 | 152 | 158 | 159 | {/* */} 160 |
    161 |
    162 |
    163 |
    164 | 165 | 168 |
    169 |
    170 | ); 171 | } 172 | 173 | export default Index; 174 | -------------------------------------------------------------------------------- /backend/routers/Question.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const mongoose = require("mongoose"); 3 | const router = express.Router(); 4 | // const mongoose = require('mongoose') 5 | const QuestionDB = require("../models/Question"); 6 | 7 | router.post("/", async (req, res) => { 8 | const questionData = new QuestionDB({ 9 | title: req.body.title, 10 | body: req.body.body, 11 | tags: req.body.tag, 12 | user: req.body.user, 13 | }); 14 | 15 | await questionData 16 | .save() 17 | .then((doc) => { 18 | res.status(201).send(doc); 19 | }) 20 | .catch((err) => { 21 | res.status(400).send({ 22 | message: "Question not added successfully", 23 | }); 24 | }); 25 | }); 26 | 27 | // router.get("/", async (req, res) => { 28 | // const questions = await QuestionDB.find({}); 29 | 30 | // try { 31 | // if (questions) { 32 | // res.status(200).send({ questions }); 33 | // } else { 34 | // res.status(400).send({ 35 | // message: "question not found", 36 | // }); 37 | // } 38 | // } catch (e) { 39 | // res.status(400).send({ 40 | // message: "Error in getting question", 41 | // }); 42 | // } 43 | // }); 44 | 45 | router.get("/:id", async (req, res) => { 46 | try { 47 | // const question = await QuestionDB.findOne({ _id: req.params.id }); 48 | // res.status(200).send(question); 49 | QuestionDB.aggregate([ 50 | { 51 | $match: { _id: mongoose.Types.ObjectId(req.params.id) }, 52 | }, 53 | { 54 | $lookup: { 55 | from: "answers", 56 | let: { question_id: "$_id" }, 57 | pipeline: [ 58 | { 59 | $match: { 60 | $expr: { 61 | $eq: ["$question_id", "$$question_id"], 62 | }, 63 | }, 64 | }, 65 | { 66 | $project: { 67 | _id: 1, 68 | user: 1, 69 | answer: 1, 70 | // created_at: 1, 71 | question_id: 1, 72 | created_at: 1, 73 | }, 74 | }, 75 | ], 76 | as: "answerDetails", 77 | }, 78 | }, 79 | { 80 | $lookup: { 81 | from: "comments", 82 | let: { question_id: "$_id" }, 83 | pipeline: [ 84 | { 85 | $match: { 86 | $expr: { 87 | $eq: ["$question_id", "$$question_id"], 88 | }, 89 | }, 90 | }, 91 | { 92 | $project: { 93 | _id: 1, 94 | question_id: 1, 95 | user: 1, 96 | comment: 1, 97 | // created_at: 1, 98 | // question_id: 1, 99 | created_at: 1, 100 | }, 101 | }, 102 | ], 103 | as: "comments", 104 | }, 105 | }, 106 | // { 107 | // $unwind: { 108 | // path: "$answerDetails", 109 | // preserveNullAndEmptyArrays: true, 110 | // }, 111 | // }, 112 | { 113 | $project: { 114 | __v: 0, 115 | // _id: "$_id", 116 | // answerDetails: { $first: "$answerDetails" }, 117 | }, 118 | }, 119 | ]) 120 | .exec() 121 | .then((questionDetails) => { 122 | res.status(200).send(questionDetails); 123 | }) 124 | .catch((e) => { 125 | console.log("Error: ", e); 126 | res.status(400).send(error); 127 | }); 128 | } catch (err) { 129 | res.status(400).send({ 130 | message: "Question not found", 131 | }); 132 | } 133 | }); 134 | 135 | router.get("/", async (req, res) => { 136 | const error = { 137 | message: "Error in retrieving questions", 138 | error: "Bad request", 139 | }; 140 | 141 | QuestionDB.aggregate([ 142 | { 143 | $lookup: { 144 | from: "comments", 145 | let: { question_id: "$_id" }, 146 | pipeline: [ 147 | { 148 | $match: { 149 | $expr: { 150 | $eq: ["$question_id", "$$question_id"], 151 | }, 152 | }, 153 | }, 154 | { 155 | $project: { 156 | _id: 1, 157 | // user_id: 1, 158 | comment: 1, 159 | created_at: 1, 160 | // question_id: 1, 161 | }, 162 | }, 163 | ], 164 | as: "comments", 165 | }, 166 | }, 167 | { 168 | $lookup: { 169 | from: "answers", 170 | let: { question_id: "$_id" }, 171 | pipeline: [ 172 | { 173 | $match: { 174 | $expr: { 175 | $eq: ["$question_id", "$$question_id"], 176 | }, 177 | }, 178 | }, 179 | { 180 | $project: { 181 | _id: 1, 182 | // user_id: 1, 183 | // answer: 1, 184 | // created_at: 1, 185 | // question_id: 1, 186 | // created_at: 1, 187 | }, 188 | }, 189 | ], 190 | as: "answerDetails", 191 | }, 192 | }, 193 | // { 194 | // $unwind: { 195 | // path: "$answerDetails", 196 | // preserveNullAndEmptyArrays: true, 197 | // }, 198 | // }, 199 | { 200 | $project: { 201 | __v: 0, 202 | // _id: "$_id", 203 | // answerDetails: { $first: "$answerDetails" }, 204 | }, 205 | }, 206 | ]) 207 | .exec() 208 | .then((questionDetails) => { 209 | res.status(200).send(questionDetails); 210 | }) 211 | .catch((e) => { 212 | console.log("Error: ", e); 213 | res.status(400).send(error); 214 | }); 215 | }); 216 | 217 | module.exports = router; 218 | -------------------------------------------------------------------------------- /frontend/src/components/Auth/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | createUserWithEmailAndPassword, 3 | signInWithEmailAndPassword, 4 | signInWithPopup, 5 | } from "firebase/auth"; 6 | import React, { useState } from "react"; 7 | import { useHistory } from "react-router-dom"; 8 | import { auth, provider } from "../../firebase"; 9 | import "./index.css"; 10 | 11 | function Index() { 12 | const history = useHistory(); 13 | const [register, setRegister] = useState(false); 14 | const [email, setEmail] = useState(""); 15 | const [password, setPassword] = useState(""); 16 | const [username, setUsername] = useState(""); 17 | const [error, setError] = useState(""); 18 | const [loading, setLoading] = useState(false); 19 | 20 | function validateEmail(email) { 21 | const reg = /^([A-Za-z0-9_\-.])+@([A-Za-z0-9_\-.])+\.([A-Za-z]{2,4})$/; 22 | if (reg.test(email) === false) { 23 | return false; 24 | } else return true; 25 | } 26 | 27 | const handleGoogleSignIN = () => { 28 | setLoading(true); 29 | signInWithPopup(auth, provider) 30 | .then((res) => { 31 | setLoading(false); 32 | // console.log(res); 33 | history.push("/"); 34 | // return ( 35 | // <> 36 | 37 | // 38 | // ); 39 | }) 40 | .catch((error) => { 41 | setLoading(false); 42 | console.log(error); 43 | }); 44 | }; 45 | 46 | const handleSignIn = () => { 47 | setError(); 48 | setLoading(true); 49 | if (email === "" || password === "") { 50 | setError("Required field is missing"); 51 | setLoading(false); 52 | } else if (!validateEmail(email)) { 53 | setError("Email is malformed"); 54 | setLoading(false); 55 | } else { 56 | signInWithEmailAndPassword(auth, email, password) 57 | .then((res) => { 58 | // console.log(res); 59 | history.push("/"); 60 | setLoading(false); 61 | }) 62 | .catch((error) => { 63 | console.log(error.code); 64 | setError(error.message); 65 | setLoading(false); 66 | }); 67 | } 68 | }; 69 | 70 | const handleRegister = () => { 71 | setError(""); 72 | setLoading(false); 73 | if (email === "" || password === "" || username === "") { 74 | setError("Required field is missing."); 75 | setLoading(false); 76 | } else if (!validateEmail(email)) { 77 | setError("Email is malformed"); 78 | setLoading(false); 79 | } else { 80 | createUserWithEmailAndPassword(auth, email, password) 81 | .then((res) => { 82 | // console.log(res); 83 | history.push("/"); 84 | setLoading(false); 85 | }) 86 | .catch((error) => { 87 | console.log(error); 88 | setError(error.message); 89 | setLoading(false); 90 | }); 91 | } 92 | }; 93 | return ( 94 |
    95 |
    96 |

    Add another way to log in using any of the following services.

    97 |
    98 |
    99 | google 103 |

    {loading ? "Signing in..." : "Login with Google"}

    104 |
    105 |
    106 | github 110 |

    Login with Github

    111 |
    112 |
    113 | facebook 117 |

    Login with Facebook

    118 |
    119 |
    120 |
    121 |
    122 | {register ? ( 123 | <> 124 | {" "} 125 |
    126 |

    Username

    127 | setUsername(e.target.value)} 130 | type="text" 131 | /> 132 |
    133 |
    134 |

    Email

    135 | setEmail(e.target.value)} 138 | type="text" 139 | /> 140 |
    141 |
    142 |

    Password

    143 | setPassword(e.target.value)} 146 | type="password" 147 | /> 148 |
    149 | 158 | 159 | ) : ( 160 | <> 161 |
    162 |

    Email

    163 | 164 |
    165 |
    166 |

    Password

    167 | 168 |
    169 | 178 | 179 | )} 180 | 181 |

    setRegister(!register)} 183 | style={{ 184 | marginTop: "10px", 185 | textAlign: "center", 186 | color: "#0095ff", 187 | textDecoration: "underline", 188 | cursor: "pointer", 189 | }} 190 | > 191 | {register ? "Login" : "Register"} ? 192 |

    193 |
    194 |
    195 | {error !== "" && ( 196 |

    202 | {error} 203 |

    204 | )} 205 |
    206 |
    207 | ); 208 | } 209 | 210 | export default Index; 211 | -------------------------------------------------------------------------------- /frontend/src/components/ViewQuestion/MainQuestion.js: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@material-ui/core"; 2 | import React, { useEffect, useState } from "react"; 3 | import BookmarkIcon from "@material-ui/icons/Bookmark"; 4 | import HistoryIcon from "@material-ui/icons/History"; 5 | import ReactQuill from "react-quill"; 6 | import Editor from "react-quill/lib/toolbar"; 7 | import axios from "axios"; 8 | import ReactHtmlParser from "react-html-parser"; 9 | import { Link } from "react-router-dom"; 10 | import "./index.css"; 11 | import { useSelector } from "react-redux"; 12 | import { selectUser } from "../../feature/userSlice"; 13 | import { stringAvatar } from "../../utils/Avatar"; 14 | 15 | function MainQuestion() { 16 | var toolbarOptions = [ 17 | ["bold", "italic", "underline", "strike"], // toggled buttons 18 | ["blockquote", "code-block"], 19 | 20 | [{ header: 1 }, { header: 2 }], // custom button values 21 | [{ list: "ordered" }, { list: "bullet" }], 22 | [{ script: "sub" }, { script: "super" }], // superscript/subscript 23 | [{ indent: "-1" }, { indent: "+1" }], // outdent/indent 24 | [{ direction: "rtl" }], // text direction 25 | 26 | // [{ size: ["small", false, "large", "huge"] }], // custom dropdown 27 | [{ header: [1, 2, 3, 4, 5, 6, false] }], 28 | 29 | [ 30 | { color: ["#ff0000", "#00ff00", "#0000ff", "#220055"] }, 31 | { background: [] }, 32 | ], // dropdown with defaults from theme 33 | [{ font: [] }], 34 | [{ align: [] }], 35 | 36 | ["clean"], // remove formatting button 37 | ]; 38 | Editor.modules = { 39 | syntax: false, 40 | toolbar: toolbarOptions, 41 | clipboard: { 42 | // toggle to add extra line breaks when pasting HTML: 43 | matchVisual: false, 44 | }, 45 | }; 46 | /* 47 | * Quill editor formats 48 | * See https://quilljs.com/docs/formats/ 49 | */ 50 | Editor.formats = [ 51 | "header", 52 | "font", 53 | "size", 54 | "bold", 55 | "italic", 56 | "underline", 57 | "strike", 58 | "blockquote", 59 | "list", 60 | "bullet", 61 | "indent", 62 | "link", 63 | "image", 64 | "video", 65 | ]; 66 | 67 | let search = window.location.search; 68 | const params = new URLSearchParams(search); 69 | const id = params.get("q"); 70 | 71 | const [questionData, setQuestionData] = useState(); 72 | const [answer, setAnswer] = useState(""); 73 | const [show, setShow] = useState(false); 74 | const [comment, setComment] = useState(""); 75 | // const [comments, setComments] = useState([]); 76 | const user = useSelector(selectUser); 77 | 78 | const handleQuill = (value) => { 79 | setAnswer(value); 80 | }; 81 | 82 | useEffect(() => { 83 | async function getFunctionDetails() { 84 | await axios 85 | .get(`/api/question/${id}`) 86 | .then((res) => setQuestionData(res.data[0])) 87 | .catch((err) => console.log(err)); 88 | } 89 | getFunctionDetails(); 90 | }, [id]); 91 | 92 | async function getUpdatedAnswer() { 93 | await axios 94 | .get(`/api/question/${id}`) 95 | .then((res) => setQuestionData(res.data[0])) 96 | .catch((err) => console.log(err)); 97 | } 98 | 99 | // console.log(questionData); 100 | const handleSubmit = async () => { 101 | const body = { 102 | question_id: id, 103 | answer: answer, 104 | user: user, 105 | }; 106 | const config = { 107 | headers: { 108 | "Content-Type": "application/json", 109 | }, 110 | }; 111 | 112 | await axios 113 | .post("/api/answer", body, config) 114 | .then(() => { 115 | alert("Answer added successfully"); 116 | setAnswer(""); 117 | getUpdatedAnswer(); 118 | }) 119 | .catch((err) => console.log(err)); 120 | }; 121 | 122 | const handleComment = async () => { 123 | if (comment !== "") { 124 | const body = { 125 | question_id: id, 126 | comment: comment, 127 | user: user, 128 | }; 129 | await axios.post(`/api/comment/${id}`, body).then((res) => { 130 | setComment(""); 131 | setShow(false); 132 | getUpdatedAnswer(); 133 | // console.log(res.data); 134 | }); 135 | } 136 | 137 | // setShow(true) 138 | }; 139 | return ( 140 |
    141 |
    142 |
    143 |

    {questionData?.title}

    144 | 145 | 146 | 147 | {/* 148 | 149 | */} 150 |
    151 |
    152 |
    153 |

    154 | Asked 155 | {new Date(questionData?.created_at).toLocaleString()} 156 |

    157 |

    158 | Activetoday 159 |

    160 |

    161 | Viewed43times 162 |

    163 |
    164 |
    165 |
    166 |
    167 |
    168 |
    169 |

    170 | 171 |

    0

    172 | 173 |

    174 | 175 | 176 | 177 | 178 |
    179 |
    180 |
    181 |

    {ReactHtmlParser(questionData?.body)}

    182 | 183 |
    184 | 185 | asked {new Date(questionData?.created_at).toLocaleString()} 186 | 187 |
    188 | 189 |

    190 | {questionData?.user?.displayName 191 | ? questionData?.user?.displayName 192 | : "Natalia lee"} 193 |

    194 |
    195 |
    196 |
    197 |
    198 | {questionData?.comments && 199 | questionData?.comments.map((_qd) => ( 200 |

    201 | {_qd.comment}{" "} 202 | 203 | - {_qd.user ? _qd.user.displayName : "Nate Eldredge"} 204 | {" "} 205 | {" "} 206 | 207 | {new Date(_qd.created_at).toLocaleString()} 208 | 209 |

    210 | ))} 211 |
    212 |

    setShow(!show)}>Add a comment

    213 | {show && ( 214 |
    215 |