From 98e0e5113a73bacab931d11c2aaee68ba090a125 Mon Sep 17 00:00:00 2001 From: balaharisankar Date: Tue, 30 Jul 2024 23:00:43 +0530 Subject: [PATCH 1/4] Upvote and Downvotes fixed --- backend/app.js | 16 +- backend/app/models/question.js | 4 + .../routes/Q&A/question/downvoteQuestion.js | 26 +-- .../app/routes/Q&A/question/upvoteQuestion.js | 2 +- frontend/src/pages/Q&A/Q&A.jsx | 2 +- frontend/src/service/Faq.jsx | 210 +++++++++--------- 6 files changed, 131 insertions(+), 129 deletions(-) diff --git a/backend/app.js b/backend/app.js index 2c6a34c5..ea39cea0 100644 --- a/backend/app.js +++ b/backend/app.js @@ -15,8 +15,20 @@ app.use(express.static('uploads')); // Set security headers app.use(helmet()); +// cookie +app.use(cookieParser()); + // CORS -app.use(cors()); +// app.use(cors()); +app.use(cors({credentials:true,origin:process.env.FRONTEND_URL})); + +app.use(function(req, res, next) { + res.header('Access-Control-Allow-Credentials', true); + res.header('Access-Control-Allow-Origin', process.env.FRONTEND_URL); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,UPDATE,OPTIONS'); + res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'); + next(); +}); // Body Parser app.use(express.json({ limit: '50mb' })); @@ -25,8 +37,6 @@ app.use(express.urlencoded({ limit: '50mb', extended: true })); // Response time app.use(responseTime({ suffix: false })); -// cookie -app.use(cookieParser()); // Use routes app.use('/', routes); diff --git a/backend/app/models/question.js b/backend/app/models/question.js index 43051a07..4fe46114 100644 --- a/backend/app/models/question.js +++ b/backend/app/models/question.js @@ -25,6 +25,10 @@ const questionSchema = new Schema( type: Number, default: 0, }, + downvotes:{ + type:Number, + default:0 + } }, { timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } } ); diff --git a/backend/app/routes/Q&A/question/downvoteQuestion.js b/backend/app/routes/Q&A/question/downvoteQuestion.js index d29b7869..3dcc23b9 100644 --- a/backend/app/routes/Q&A/question/downvoteQuestion.js +++ b/backend/app/routes/Q&A/question/downvoteQuestion.js @@ -6,25 +6,11 @@ const { getVoteCookieName } = require('../../../../helpers/middlewares/cookie'); module.exports = async (req, res, next) => { const { questionId } = req.body; - const [err] = await to( - question.updateOne({ _id: questionId }, [ - { - $set: { - upvotes: { - $cond: [ - { - $gt: ['$upvotes', 0], - }, - { - $subtract: ['$upvotes', 1], - }, - 0, - ], - }, - }, - }, - ]) - ); + const existingQues=await question.findById(questionId) + if(!existingQues.downvotes){ + const [err] = await to(question.updateOne({ _id: questionId },{$set:{downvotes:0}})); + } + const [err] = await to(question.updateOne({ _id: questionId }, { $inc: { downvotes: 1 } })); if (err) { console.log(err); const error = new ErrorHandler(constants.ERRORS.DATABASE, { @@ -36,7 +22,7 @@ module.exports = async (req, res, next) => { return next(error); } - res.cookie(getVoteCookieName('question', questionId), true, { maxAge: 20 * 365 * 24 * 60 * 60 * 1000 }); + res.cookie(getVoteCookieName('question', questionId), true, { maxAge: 20 * 365 * 24 * 60 * 60 * 1000, sameSite: "none", secure: true }); res.status(200).send({ message: 'Question has been down voted', }); diff --git a/backend/app/routes/Q&A/question/upvoteQuestion.js b/backend/app/routes/Q&A/question/upvoteQuestion.js index 77a1b2a2..671dca9d 100644 --- a/backend/app/routes/Q&A/question/upvoteQuestion.js +++ b/backend/app/routes/Q&A/question/upvoteQuestion.js @@ -16,7 +16,7 @@ module.exports = async (req, res, next) => { return next(error); } - res.cookie(getVoteCookieName('question', questionId), true, { maxAge: 20 * 365 * 24 * 60 * 60 * 1000 }); + res.cookie(getVoteCookieName('question', questionId), true, { maxAge: 20 * 365 * 24 * 60 * 60 * 1000,sameSite:"none",secure:true }); res.status(200).send({ message: 'Question has been upvoted', diff --git a/frontend/src/pages/Q&A/Q&A.jsx b/frontend/src/pages/Q&A/Q&A.jsx index d9d2e1bb..0423c21d 100644 --- a/frontend/src/pages/Q&A/Q&A.jsx +++ b/frontend/src/pages/Q&A/Q&A.jsx @@ -191,7 +191,7 @@ function Ques(props) { className="vote-btn" onClick={() => handleDownvote(item._id)} > - 👎 {item?.downvote} + 👎 {item?.downvotes} diff --git a/frontend/src/service/Faq.jsx b/frontend/src/service/Faq.jsx index f23e7c8b..d3ee876c 100644 --- a/frontend/src/service/Faq.jsx +++ b/frontend/src/service/Faq.jsx @@ -2,127 +2,127 @@ import { END_POINT } from "../config/api"; import { showToast } from "./toastService"; export async function postFaq(formData, setToast, toast) { - try { - const response = await fetch(`${END_POINT}/faq/postFaq`, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify(formData), + try { + const response = await fetch(`${END_POINT}/faq/postFaq`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify(formData), + }); + + if (response.ok) { + setToast({ + ...toast, + toastMessage: "FAQ has been added", + toastStatus: true, + toastType: "success", }); - - if (response.ok) { - setToast({ - ...toast, - toastMessage: "FAQ has been added", - toastStatus: true, - toastType: "success", - }); - return { success: true }; - } else { - setToast({ - ...toast, - toastMessage: "Database Error", - toastStatus: true, - toastType: "error", - }); - return { success: false, error: "Database Error" }; - } - } catch (error) { + return { success: true }; + } else { setToast({ ...toast, - toastMessage: "Network Error", + toastMessage: "Database Error", toastStatus: true, toastType: "error", }); - return { success: false, error: "Network Error" }; + return { success: false, error: "Database Error" }; } + } catch (error) { + setToast({ + ...toast, + toastMessage: "Network Error", + toastStatus: true, + toastType: "error", + }); + return { success: false, error: "Network Error" }; } +} export async function getFaq() { - try { - const response = await fetch(`${END_POINT}/faq/getFaq`); - if (!response.ok) { - throw new Error("Failed to fetch FAQs"); - } - const data = await response.json(); - return data.Faq; - } catch (error) { - console.error("Failed to fetch FAQs:", error.message); + try { + const response = await fetch(`${END_POINT}/faq/getFaq`); + if (!response.ok) { throw new Error("Failed to fetch FAQs"); } + const data = await response.json(); + return data.Faq; + } catch (error) { + console.error("Failed to fetch FAQs:", error.message); + throw new Error("Failed to fetch FAQs"); + } } -export const deleteFaq = async (faqId, setToast, toast) => { - const url = `${END_POINT}/faq/deleteFaq`; - const body = { faqId: faqId }; - const headers = { - "Content-Type": "application/json", - authorization: `Bearer ${localStorage.getItem("token")}`, - }; - try { - const response = await fetch(url, { - method: "PUT", - headers: headers, - body: JSON.stringify(body), - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - setToast({ - ...toast, - toastMessage: data.message, - toastStatus: true, - toastType: "success", - }); - return data.message; - } catch (error) { - console.error("Failed to delete FAQ:", error.message); - setToast({ - ...toast, - toastMessage: "Failed to delete FAQ", - toastStatus: true, - toastType: "error", - }); - throw new Error("Failed to delete FAQ"); +export const deleteFaq = async (faqId, setToast, toast) => { + const url = `${END_POINT}/faq/deleteFaq`; + const body = { faqId: faqId }; + const headers = { + "Content-Type": "application/json", + authorization: `Bearer ${localStorage.getItem("token")}`, + }; + try { + const response = await fetch(url, { + method: "PUT", + headers: headers, + body: JSON.stringify(body), + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } + const data = await response.json(); + setToast({ + ...toast, + toastMessage: data.message, + toastStatus: true, + toastType: "success", + }); + return data.message; + } catch (error) { + console.error("Failed to delete FAQ:", error.message); + setToast({ + ...toast, + toastMessage: "Failed to delete FAQ", + toastStatus: true, + toastType: "error", + }); + throw new Error("Failed to delete FAQ"); + } }; export const updateFaq = async (faqId, updatedFaqDetails, setToast, toast) => { - try { - const response = await fetch(`${END_POINT}/faq/updateFaq`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - authorization: `Bearer ${localStorage.getItem("token")}`, - }, - body: JSON.stringify({ faqId, ...updatedFaqDetails }), - }); - - if (!response.ok) { - throw new Error("Failed to update FAQ"); - } + try { + const response = await fetch(`${END_POINT}/faq/updateFaq`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + authorization: `Bearer ${localStorage.getItem("token")}`, + }, + body: JSON.stringify({ faqId, ...updatedFaqDetails }), + }); - const data = await response.json(); - setToast({ - ...toast, - toastMessage: data.message, - toastStatus: true, - toastType: "success", - }); - return data.message; - } catch (error) { - console.error("Failed to update FAQ:", error.message); - setToast({ - ...toast, - toastMessage: "Failed to update FAQ", - toastStatus: true, - toastType: "error", - }); - throw new Error("Failed to update FAQ"); + if (!response.ok) { + throw new Error("Failed to update FAQ"); } + + const data = await response.json(); + setToast({ + ...toast, + toastMessage: data.message, + toastStatus: true, + toastType: "success", + }); + return data.message; + } catch (error) { + console.error("Failed to update FAQ:", error.message); + setToast({ + ...toast, + toastMessage: "Failed to update FAQ", + toastStatus: true, + toastType: "error", + }); + throw new Error("Failed to update FAQ"); + } }; export const getAllQuestions = async (setToast, toast) => { @@ -292,6 +292,7 @@ export const upvote = async (questionId, handleToast) => { headers: { "Content-Type": "application/json", }, + credentials: "include", body: JSON.stringify({ questionId }), }); if (!response.ok) { @@ -300,7 +301,7 @@ export const upvote = async (questionId, handleToast) => { showToast(handleToast, "Upvote Successfully"); return response.json(); } catch (error) { - showToast(handleToast, "Failed to upvote question", "error"); + showToast(handleToast, "You have already voted", "error"); throw new Error("Failed to upvote question"); } }; @@ -312,6 +313,7 @@ export const downvote = async (questionId, handleToast) => { headers: { "Content-Type": "application/json", }, + credentials: "include", body: JSON.stringify({ questionId }), }); if (!response.ok) { @@ -320,7 +322,7 @@ export const downvote = async (questionId, handleToast) => { showToast(handleToast, "Downvote Successfully"); return response.json(); } catch (error) { - showToast(handleToast, "Failed to downvote question", "error"); + showToast(handleToast, "You have already voted", "error"); throw new Error("Failed to downvote question"); } -}; \ No newline at end of file +}; From bc5fd09d82ec82fe2089b01d92140fbc1617bb26 Mon Sep 17 00:00:00 2001 From: balaharisankar Date: Wed, 7 Aug 2024 01:04:52 +0530 Subject: [PATCH 2/4] Feat:UI Enhanced in Manage Q&A Section of Admin Panel --- .../Faq/Q&A/AnswersModel/AnswersModel.jsx | 112 ++++++++++++++++ .../Faq/Q&A/AnswersModel/AnswersModel.scss | 104 +++++++++++++++ .../Components/Faq/Q&A/AnswersModel/index.js | 1 + .../pages/Admin/Components/Faq/Q&A/QandA.jsx | 124 +++--------------- 4 files changed, 237 insertions(+), 104 deletions(-) create mode 100644 frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx create mode 100644 frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.scss create mode 100644 frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/index.js diff --git a/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx new file mode 100644 index 00000000..4b67cb17 --- /dev/null +++ b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx @@ -0,0 +1,112 @@ +import React, { useEffect, useState } from "react"; +import { Modal, Backdrop, Fade } from '@material-ui/core'; +import { SimpleToast } from "../../../../../../components/util/Toast"; +import { getAnswers, updateAnswerStatus, deleteAnswer } from "../../../../../../service/Faq"; +import style from './AnswersModel.scss' + +export function AnswersModel(props) { + let dark = props.theme + const [answers, setAnswers] = useState([]) + const [toast, setToast] = useState({ + toastStatus: false, + toastType: "", + toastMessage: "", + }); + async function fetchAnswers() { + const data = await getAnswers(props.data._id, setToast) + setAnswers(data) + } + const updateAnswer = async (id, status) => { + setToast({ toastStatus: true, toastMessage: "Loading...", toastType: "info" }) + await updateAnswerStatus(id, status, setToast); + fetchAnswers() + } + const handleDeleteAnswer = async (answerId) => { + setToast({ toastStatus: true, toastMessage: "Loading...", toastType: "info" }) + await deleteAnswer(answerId, setToast) + fetchAnswers() + } + useEffect(() => { + if (props.open) + fetchAnswers() + }, [props]) + function timeStampFormatter(time) { + const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + const messageTime = new Date(time) + return `${String(messageTime.getDate())} ${String(months[messageTime.getMonth()])} ${String(messageTime.getFullYear())}` + } + return ( +
+ {toast.toastStatus && ( + { setToast({ toastMessage: "", toastStatus: false, toastType: "" }) }} + severity={toast.toastType} + /> + )} + + +
+
+ { + setToast({ toastMessage: "", toastStatus: false, toastType: "" }) + props.handleClose(false) + }}> + + +
+ { + answers.length == 0 ? +

No answers found...

+ : +
+ { + answers.map((ans, index) => { + return ( +
+
+
{ans.created_by || "Anonymous"}
+

{timeStampFormatter(ans.created_on)}

+
+

{ans.answer}

+
+ + {ans.isApproved == true && + } +
+
+ ) + }) + } +
+ } +
+
+
+
+ ) +} diff --git a/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.scss b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.scss new file mode 100644 index 00000000..e5ddb69f --- /dev/null +++ b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.scss @@ -0,0 +1,104 @@ +:root { + font-family: "Futura LT Book"; +} + +.modal-container { + width: 70%; + height: auto; + background-color: white; + outline: none !important; + padding: 20px 20px; + border-radius: 12px; + + h2 { + margin: 0; + } + + max-height: 100%; + overflow-y: scroll; +} + +.modal { + display: flex; + position: fixed; + align-items: center; + justify-content: center; + align-items: center; + overflow-y: scroll; + outline: none !important; +} + +.close-icon-container { + display: flex; + justify-content: end; +} + +.close-icon { + font-size: 24px; + padding-right: 20px; + cursor: pointer; + color: #243e74; +} + +.answer-container { + width: 100%; + border-bottom: 1px solid #ccc; + padding-bottom: 20px; + margin-top: 20px; + + p { + margin-top: 8px; + margin-bottom: 8px; + font-size: 16px; + } +} + +.button-group { + display: flex; + width: 100%; + align-items: center; + gap: 10px; +} + +.button-approve { + padding: 10px; + border: none; + outline: none; + border-radius: 5px; + background-color: rgb(6, 158, 41); + margin: 5px; + color: #fff; + width: 120px; + font-size: 12px; + font-weight: bold; + transition: background-color 200ms; +} + +.button-delete { + padding: 10px; + border: none; + outline: none; + border-radius: 5px; + background-color: #fc0254; + margin: 5px; + color: #fff; + width: 120px; + font-size: 12px; + font-weight: bold; + transition: background-color 200ms; + text-align: center; +} + +.button-delete:hover { + background-color: #fc3779; +} + +@media screen and (max-width:768px) { + .modal-container { + width: 90%; + } + + .close-icon { + padding-right: 5px; + } +} \ No newline at end of file diff --git a/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/index.js b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/index.js new file mode 100644 index 00000000..adb6d86f --- /dev/null +++ b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/index.js @@ -0,0 +1 @@ +export * from './AnswersModel' \ No newline at end of file diff --git a/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx b/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx index 8d4eb61d..54a8dff4 100644 --- a/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx +++ b/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx @@ -2,21 +2,16 @@ import React, { useEffect, useState } from "react"; import style from "./qanda.module.scss"; import { getAllQuestions, - deleteAnswer, deleteQuestion, updateQuestionStatus, - updateAnswerStatus, - getAnswers, } from "../../../../../service/Faq"; import { SimpleToast } from "../../../../../components/util/Toast/Toast"; -import Loader from "../../../../../components/util/Loader"; -import { hideToast, showToast } from "../../../../../service/toastService"; +import { hideToast } from "../../../../../service/toastService"; import Modal from "@material-ui/core/Modal"; import Button from "@material-ui/core/Button"; import Typography from "@material-ui/core/Typography"; import { makeStyles } from "@material-ui/core/styles"; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import { AnswersModel } from "./AnswersModel/AnswersModel"; const useStyles = makeStyles((theme) => ({ modal: { @@ -40,8 +35,8 @@ const useStyles = makeStyles((theme) => ({ export function QandA() { const [cards, setCards] = useState([]); - const [expandedCards, setExpandedCards] = useState({}); - const [isLoaded, setIsLoaded] = useState(false); + const [open, setOpen] = useState(false) + const [currentQuesId, setCurrentQuesId] = useState("") const [toast, setToast] = useState({ toastStatus: false, toastType: "", @@ -52,10 +47,9 @@ export function QandA() { const classes = useStyles(); const getQuestions = async () => { - setIsLoaded(true); + setToast({ toastStatus: true, toastMessage: "Loading...", toastType: "info" }) const data = await getAllQuestions(setToast, toast); setCards(data); - setIsLoaded(false); }; const handleOpenConfirmModal = (id) => { @@ -82,58 +76,11 @@ export function QandA() { await updateQuestionStatus(id, status, setToast); }; - const handleDeleteAnswer = async (answerId) => { - const questionId = Object.keys(expandedCards)[0]; - const prevAnswers = expandedCards[questionId]?.answers || []; - - setExpandedCards((prev) => ({ - ...prev, - [questionId]: { - ...prev[questionId], - answers: prevAnswers.filter(answer => answer._id !== answerId), - }, - })); - - await deleteAnswer(answerId, setToast); - }; - - const updateAnswer = async (id, status) => { - const questionId = Object.keys(expandedCards)[0]; - const prevAnswers = expandedCards[questionId]?.answers || []; - - setExpandedCards((prev) => ({ - ...prev, - [questionId]: { - ...prev[questionId], - answers: prevAnswers.map(answer => - answer._id === id ? { ...answer, isApproved: status } : answer - ), - }, - })); - - await updateAnswerStatus(id, status, setToast); - }; - const handleToggleExpand = async (id) => { - if (expandedCards[id]) { - setExpandedCards((prev) => { - const newExpanded = { ...prev }; - delete newExpanded[id]; - return newExpanded; - }); - } else { - setIsLoaded(true); - const aRes = await getAnswers(id, setToast); - setExpandedCards((prev) => ({ - ...prev, - [id]: { - answers: aRes, - }, - })); - setIsLoaded(false); - } + setCurrentQuesId(id) + setOpen(true) }; - + const handleCloseToast = (event, reason) => { if (reason === "clickaway") { @@ -148,8 +95,8 @@ export function QandA() { return (
+

Manage Q&A

-
{isLoaded && }
{cards?.map((qns, index) => (
@@ -172,55 +119,24 @@ export function QandA() { > {qns?.isApproved ? "Reject" : "Approve"} - + { + qns.isApproved == true && + + }
- {expandedCards[qns._id] && ( -
- {expandedCards[qns._id].answers?.length === 0 ? ( - No answers Found - ) : ( - expandedCards[qns._id].answers.map((a) => ( -
-
-
-

{a.answer}

-
- - -
-
-
-
- )) - )} -
- )}
))}
From 843e362cbd1475ab6b73f540f998dec44d2a636b Mon Sep 17 00:00:00 2001 From: balaharisankar Date: Wed, 7 Aug 2024 13:09:10 +0530 Subject: [PATCH 3/4] buttons corrected --- .../Faq/Q&A/AnswersModel/AnswersModel.jsx | 43 ++++++++++++------- .../pages/Admin/Components/Faq/Q&A/QandA.jsx | 40 ++++++++++------- 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx index 4b67cb17..e091b144 100644 --- a/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx +++ b/frontend/src/pages/Admin/Components/Faq/Q&A/AnswersModel/AnswersModel.jsx @@ -82,21 +82,34 @@ export function AnswersModel(props) {

{ans.answer}

- - {ans.isApproved == true && - } + { + ans.isApproved ? + + : + <> + + + + + }
) diff --git a/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx b/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx index 54a8dff4..0f284505 100644 --- a/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx +++ b/frontend/src/pages/Admin/Components/Faq/Q&A/QandA.jsx @@ -112,22 +112,32 @@ export function QandA() {
- { - qns.isApproved == true && - + qns.isApproved ? + + : + <> + + + }