Skip to content

Commit 76fdc7e

Browse files
committed
added dashboard, filters in questions
1 parent fcced2e commit 76fdc7e

File tree

13 files changed

+252
-91
lines changed

13 files changed

+252
-91
lines changed

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,18 @@ A place to share and organize knowledge where you can ask or answer questions.
2121
- Signup
2222
- Login
2323
- Logout
24-
- Users can ask question
24+
- Forgot and reset password
25+
- Users can ask questions providing the title and body with formatting as well.
2526
- Users can edit the questions asked by them.
26-
- The already asked questions with their answers can be viewed by any type of person (non-logged persons as well).
27+
- The already asked questions with their answers can be viewed by all people.
28+
- Users can answer the questions by selecting any from all the questions present.
2729
- Users can also view questions specifically asked by them.
30+
- Users can view and edit their profile. This also includes changing their username and password as well.
2831
- Users can bookmark/unbookmark a question or a specific answer.
32+
- Users can view the bookmarks on separate page, where they can directly toggle those.
33+
- Users can like the questions/answers of others. The like count is publicly visible.
34+
- All the likes of a user can also be accessed separately.
35+
- Users can view their activities in separate page.
2936
- Link to question or a specific answer to question can also be copied.
3037
- Social sharing is also possible.
3138
- Questions along with the answers can also be downloaded.
@@ -44,6 +51,7 @@ A place to share and organize knowledge where you can ask or answer questions.
4451
- Prompt login modal when non-logged persons try to perform auth activity
4552
- Responsive Sidebar with toggle option
4653
- Slugs used for SEO friendly URLs
54+
- Search params used for applying filters
4755
- Toasts for success and error messages
4856
- Use of 404 page for wrong urls
4957
- Use of layout component for pages

backend/ApiDocs.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,10 @@ GET /api/answers/me
1414
- Access: Private
1515
- Description: gets the answers previously answered by current logged-in user
1616

17-
GET /api/answers/byQuestion/:qid
18-
- Access: Public
19-
- Description: gets the answers of a question by the question id
20-
2117
GET /api/answers/:ansid
2218
- Access: Public
2319
- Description: gets an answer by id
2420

25-
POST /api/answers/:qid
26-
- body: { text }
27-
- headers: { Authorization: "jwt-token" }
28-
- Access: Private
29-
- Description: posts the answer to the question passed by its id in the name of current logged-in user
30-
3121
PUT /api/answers/:ansid
3222
- body: { text }
3323
- headers: { Authorization: "jwt-token" }
@@ -91,6 +81,14 @@ DELETE /api/bookmarks/:bookmarkId
9181

9282

9383

84+
## Dashboard Routes
85+
GET /api/dashboard
86+
- headers: { Authorization: "jwt-token" }
87+
- Access: Private
88+
- Description: gets the personalized dashboard data for the user
89+
90+
91+
9492
## Like Routes
9593
GET /api/likes/questions/:qid
9694
- Access: Public
@@ -173,6 +171,16 @@ PUT /api/questions/:qid
173171
- Access: Private
174172
- Description: updates the question by id
175173

174+
GET /api/questions/:qid/answers
175+
- Access: Public
176+
- Description: gets the answers of a question by the question id
177+
178+
POST /api/questions/:qid/answers
179+
- body: { text }
180+
- headers: { Authorization: "jwt-token" }
181+
- Access: Private
182+
- Description: posts the answer to the question passed by its id in the name of current logged-in user
183+
176184
GET /api/questions/byslug/:qslug
177185
- Access: Public
178186
- Description: gets the question by its slug

backend/controllers/answerControllers.js

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,6 @@ const activityEnum = require("../utils/activityEnum");
55
const { validateObjectId } = require("../utils/validation");
66

77

8-
exports.getAnswersByQuestion = async (req, res) => {
9-
try {
10-
const questionId = req.params.qid;
11-
12-
if (!validateObjectId(questionId)) {
13-
return res.status(400).json({ msg: "Question id not valid" });
14-
}
15-
16-
const question = await Question.findById(questionId);
17-
if (!question) {
18-
return res.status(400).json({ msg: "No question found.." });
19-
}
20-
21-
const answers = await Answer.find({ question: questionId }).populate("answerer", "-password");
22-
res.status(200).json({ answers, msg: "Answers found successfully" });
23-
}
24-
catch (err) {
25-
console.error(err);
26-
return res.status(500).json({ msg: "Internal Server Error" });
27-
}
28-
}
29-
308

319
exports.getAnswerById = async (req, res) => {
3210
try {
@@ -49,41 +27,6 @@ exports.getAnswerById = async (req, res) => {
4927
}
5028

5129

52-
exports.postAnswer = async (req, res) => {
53-
try {
54-
const questionId = req.params.qid;
55-
const { text } = req.body;
56-
const userId = req.user.id;
57-
if (!text) {
58-
return res.status(400).json({ msg: "Answer can't be empty" });
59-
}
60-
61-
if (!validateObjectId(questionId)) {
62-
return res.status(400).json({ msg: "invalid Question id" });
63-
}
64-
65-
const question = await Question.findById(questionId);
66-
if (!question) {
67-
return res.status(400).json({ msg: "No question found.." });
68-
}
69-
70-
if (question.questioner == req.user.id) {
71-
return res.status(400).json({ msg: "You can't post answer to your own question!!" });
72-
}
73-
74-
const answer = await Answer.create({ question: questionId, answerer: userId, text });
75-
await Activity.create({ user: req.user.id, activityType: activityEnum.CREATED_ANSWER, answer: answer._id });
76-
77-
res.status(200).json({ answer, msg: "Answer posted successfully" });
78-
79-
}
80-
catch (err) {
81-
console.error(err);
82-
return res.status(500).json({ msg: "Internal Server Error" });
83-
}
84-
}
85-
86-
8730

8831
exports.updateAnswerById = async (req, res) => {
8932
try {

backend/controllers/questionControllers.js

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ const { validateObjectId } = require("../utils/validation");
99
exports.getQuestions = async (req, res) => {
1010
try {
1111

12-
const page = parseInt(req.query.page, 10) || 1;
13-
const pageSize = parseInt(req.query.pageSize, 10) || 10;
12+
let { answerFilter } = req.query;
13+
1414

1515
let questions = await Question
1616
.find()
17-
.skip((page - 1) * pageSize)
18-
.limit(pageSize)
1917
.sort("-createdAt")
2018
.populate("questioner", "name")
2119
.lean();
@@ -28,6 +26,17 @@ exports.getQuestions = async (req, res) => {
2826
}
2927

3028
questions = await Promise.all(questions.map(question => addExtraInfo(question)));
29+
30+
if (answerFilter == "noAnswer") {
31+
questions = questions.filter(question => question.ansCount == 0);
32+
}
33+
else if (answerFilter == "hasAnswer") {
34+
questions = questions.filter(question => question.ansCount > 0);
35+
}
36+
else if (answerFilter == "hasAcceptedAnswer") {
37+
questions = questions.filter(question => question.acceptedAnsCount > 0);
38+
}
39+
3140
res.status(200).json({ questions, msg: "Questions found successfully" });
3241
}
3342
catch (err) {
@@ -179,3 +188,61 @@ exports.getQuestionsOfCurrentUser = async (req, res) => {
179188
}
180189
}
181190

191+
192+
exports.getAnswersByQuestion = async (req, res) => {
193+
try {
194+
const questionId = req.params.qid;
195+
196+
if (!validateObjectId(questionId)) {
197+
return res.status(400).json({ msg: "Question id not valid" });
198+
}
199+
200+
const question = await Question.findById(questionId);
201+
if (!question) {
202+
return res.status(400).json({ msg: "No question found.." });
203+
}
204+
205+
const answers = await Answer.find({ question: questionId }).populate("answerer", "-password");
206+
res.status(200).json({ answers, msg: "Answers found successfully" });
207+
}
208+
catch (err) {
209+
console.error(err);
210+
return res.status(500).json({ msg: "Internal Server Error" });
211+
}
212+
}
213+
214+
215+
exports.postAnswer = async (req, res) => {
216+
try {
217+
const questionId = req.params.qid;
218+
const { text } = req.body;
219+
const userId = req.user.id;
220+
if (!text) {
221+
return res.status(400).json({ msg: "Answer can't be empty" });
222+
}
223+
224+
if (!validateObjectId(questionId)) {
225+
return res.status(400).json({ msg: "invalid Question id" });
226+
}
227+
228+
const question = await Question.findById(questionId);
229+
if (!question) {
230+
return res.status(400).json({ msg: "No question found.." });
231+
}
232+
233+
if (question.questioner == req.user.id) {
234+
return res.status(400).json({ msg: "You can't post answer to your own question!!" });
235+
}
236+
237+
const answer = await Answer.create({ question: questionId, answerer: userId, text });
238+
await Activity.create({ user: req.user.id, activityType: activityEnum.CREATED_ANSWER, answer: answer._id });
239+
240+
res.status(200).json({ answer, msg: "Answer posted successfully" });
241+
242+
}
243+
catch (err) {
244+
console.error(err);
245+
return res.status(500).json({ msg: "Internal Server Error" });
246+
}
247+
}
248+

backend/routes/answerRoutes.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
const express = require("express");
2-
const { getAnswersByQuestion, postAnswer, updateAnswerById, getAnswersOfCurrentUser, getAnswerById, acceptAnswer } = require("../controllers/answerControllers");
2+
const { updateAnswerById, getAnswersOfCurrentUser, getAnswerById, acceptAnswer } = require("../controllers/answerControllers");
33
const router = express.Router();
44
const { verifyAccessToken } = require("../middlewares");
55

66

77
// Routes beginning with /api/answers
88
router.get("/me", verifyAccessToken, getAnswersOfCurrentUser);
9-
router.get("/byQuestion/:qid", getAnswersByQuestion);
109
router.get("/:ansid", getAnswerById);
11-
router.post("/:qid", verifyAccessToken, postAnswer);
1210
router.put("/:ansid", verifyAccessToken, updateAnswerById);
1311
router.put("/:ansid/accept", verifyAccessToken, acceptAnswer);
1412

backend/routes/questionRoutes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const express = require("express");
22
const router = express.Router();
3-
const { getQuestions, getQuestionById, postQuestion, updateQuestionById, getQuestionBySlug, getQuestionsOfCurrentUser } = require("../controllers/questionControllers");
3+
const { getQuestions, getQuestionById, postQuestion, updateQuestionById, getQuestionBySlug, getQuestionsOfCurrentUser, getAnswersByQuestion, postAnswer } = require("../controllers/questionControllers");
44
const { verifyAccessToken } = require("../middlewares");
55

66

@@ -11,6 +11,8 @@ router.get("/me", verifyAccessToken, getQuestionsOfCurrentUser);
1111

1212
router.get("/:qid", getQuestionById);
1313
router.put("/:qid", verifyAccessToken, updateQuestionById);
14+
router.get("/:qid/answers", getAnswersByQuestion);
15+
router.post("/:qid/answers", verifyAccessToken, postAnswer);
1416

1517
router.get("/byslug/:qslug", getQuestionBySlug);
1618

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react'
2+
import { useState } from 'react'
3+
import RadioBtn from './utils/RadioBtn';
4+
5+
const QuestionFilters = ({ searchParams, setSearchParams }) => {
6+
7+
const currSearchParams = Object.fromEntries([...searchParams]);
8+
const { answerFilter } = currSearchParams;
9+
10+
const [formData, setFormData] = useState({
11+
answerFilter: answerFilter || "", // value belongs to [noAnswer, hasAnswer, hasAcceptedAnswer]
12+
});
13+
14+
const handleAnswerFilterChange = (name, value, checked) => {
15+
if (!checked)
16+
setFormData({ ...formData, answerFilter: "" });
17+
else
18+
setFormData({ ...formData, answerFilter: value });
19+
}
20+
21+
const handleSubmit = async e => {
22+
e.preventDefault();
23+
if (formData.answerFilter) {
24+
setSearchParams({ ...currSearchParams, answerFilter: formData.answerFilter });
25+
}
26+
else {
27+
searchParams.delete("answerFilter");
28+
setSearchParams(searchParams);
29+
}
30+
}
31+
32+
return (
33+
<>
34+
<div className='sm:mx-8 p-4 bg-sky-50 dark:bg-gray-800 border-2 border-sky-200 dark:border-none rounded-md'>
35+
<h4 className='text-lg'>Filters</h4>
36+
37+
<form>
38+
<div>
39+
<RadioBtn label="No answer" id="no-answer" name="answerFilter" value='noAnswer' checked={formData.answerFilter === "noAnswer"} onChange={handleAnswerFilterChange} />
40+
<RadioBtn label="Has answer" id="has-answer" name="answerFilter" value="hasAnswer" checked={formData.answerFilter === "hasAnswer"} onChange={handleAnswerFilterChange} />
41+
<RadioBtn label="Has accepted answer" id="has-accepted-answer" name="answerFilter" value="hasAcceptedAnswer" checked={formData.answerFilter === "hasAcceptedAnswer"} onChange={handleAnswerFilterChange} />
42+
</div>
43+
<button className='px-3 py-1.5 font-semibold dark:text-black bg-sky-400 hover:bg-sky-500 transition rounded-[3px]' onClick={handleSubmit}>Apply</button>
44+
</form>
45+
46+
</div>
47+
</>
48+
)
49+
}
50+
51+
export default QuestionFilters

frontend/src/components/forms/PostAnswerForm.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const PostAnswerForm = ({ questionId, onSuccessPost }) => {
3131
return;
3232
}
3333

34-
const config = { url: `/answers/${questionId}`, method: "post", data: formData, headers: { Authorization: authState.token } };
34+
const config = { url: `/questions/${questionId}/answers`, method: "post", data: formData, headers: { Authorization: authState.token } };
3535
fetchData(config).then(() => {
3636
onSuccessPost();
3737
});
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { useEffect, useState } from 'react'
2+
3+
const Checkbox = ({ label, id, name, value, checked = false, indeterminate = false, onChange }) => {
4+
let [isChecked, setIsChecked] = useState(checked);
5+
const [isIndeterminate, setIsIndeterminate] = useState(indeterminate);
6+
const handleCheckboxClick = () => {
7+
setIsIndeterminate(false);
8+
setIsChecked(!isChecked);
9+
}
10+
11+
useEffect(() => setIsChecked(checked), [checked]);
12+
useEffect(() => setIsIndeterminate(indeterminate), [indeterminate]);
13+
useEffect(() => {
14+
if (isIndeterminate) return;
15+
onChange(name, value, isChecked)
16+
}, [isChecked, isIndeterminate, name, value, onChange]);
17+
18+
19+
return (
20+
<>
21+
<label htmlFor={id} className='inline-flex items-center cursor-pointer group'>
22+
<span className='relative w-10 h-10 p-2 rounded-full transition group-hover:bg-gray-100 dark:group-hover:bg-gray-900'>
23+
<input type="checkbox" id={id} name={name} value={value} className='absolute peer w-full h-full top-0 left-0 opacity-0 cursor-pointer' onChange={handleCheckboxClick} />
24+
{isIndeterminate ? (
25+
<svg className='w-6 h-6 absolute z-10 fill-teal-600' focusable="false" aria-hidden="true" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"></path></svg>
26+
) : isChecked ? (
27+
<svg className='w-6 h-6 absolute z-10 fill-teal-600' focusable="false" aria-hidden="true" viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"></path></svg>
28+
) : (
29+
<svg className="w-6 h-6 absolute z-10 fill-gray-700" focusable="false" aria-hidden="true" viewBox="0 0 24 24"> <path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"></path></svg>
30+
)}
31+
<span className='absolute top-0 left-0 w-full h-full rounded-full transition duration-500 scale-0 peer-active:scale-100 peer-active:bg-gray-200 peer-focus:scale-100 peer-focus:bg-gray-200 dark:peer-focus:bg-gray-900'></span>
32+
</span>
33+
34+
<span>{label}</span>
35+
</label>
36+
</>
37+
)
38+
}
39+
40+
export default Checkbox

0 commit comments

Comments
 (0)