diff --git a/client/package.json b/client/package.json
index 289fe82..6fe2359 100644
--- a/client/package.json
+++ b/client/package.json
@@ -16,6 +16,7 @@
"js-cookie": "^3.0.1",
"marked": "^4.0.18",
"react": "^18.2.0",
+ "react-copy-to-clipboard": "^5.1.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
diff --git a/client/src/components/Badges.jsx b/client/src/components/Badges.jsx
new file mode 100644
index 0000000..29f6611
--- /dev/null
+++ b/client/src/components/Badges.jsx
@@ -0,0 +1,47 @@
+import WorkspacePremiumIcon from '@mui/icons-material/WorkspacePremium';
+import { Grid, Tooltip, Typography } from '@mui/material';
+
+function Badge({
+ title = 'Good Question',
+ text = 'dadsfjads;fkj',
+ rank = 'Gold'
+}) {
+ const color = {
+ Gold: '#ffd700',
+ Silver: '#c0c0c0',
+ Bronze: '#cd7f32'
+ }
+
+ return (
+
+
+
+ {title}
+
+
+ );
+}
+export default function Badges({ badges: { obtained, unobtained } }) {
+ const badgesGrid = (badges) => badges.map((badge) => (
+
+
+
+ ))
+
+ return (
+ <>
+
+
+ Obtained Badges
+
+ {badgesGrid(obtained)}
+
+
+
+ Unobtained Badges
+
+ {badgesGrid(unobtained)}
+
+ >
+ );
+}
diff --git a/client/src/components/ListQuestion.jsx b/client/src/components/ListQuestion.jsx
index 62b97a4..9240bb0 100644
--- a/client/src/components/ListQuestion.jsx
+++ b/client/src/components/ListQuestion.jsx
@@ -1,4 +1,4 @@
-import { Divider, Grid, ListItem, Stack, Typography } from '@mui/material';
+import { Divider, Grid, ListItem, Stack, Typography, Chip } from '@mui/material';
import { Link } from 'react-router-dom';
import { CreationInfoTag } from 'controllers';
@@ -11,24 +11,52 @@ export default function ListQuestion({
creator,
downvotes,
status,
+ tags,
text,
title,
upvotes,
views,
+ hasAcceptedAnswer,
}) {
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
+
return (
-
-
- {sm || md ? : null}
-
-
+ {(sm || md) && (
+
+
+
+
+
+ )}
+
- {sm || md ? null: }
+ {sm || md ? null : (
+
+ )}
- [{status}] {title}
+ [{status}] {title}{' '}
+
{text.replace(/<[^>]*>?/gm, '')}
+ {tags.length > 0 && (
+
+ Tags:{' '}
+ {tags.map((tag, index) => (
+
+ ))}
+
+ )}
diff --git a/client/src/components/ListQuestionInfo.jsx b/client/src/components/ListQuestionInfo.jsx
index 57c1a87..f85dc6d 100644
--- a/client/src/components/ListQuestionInfo.jsx
+++ b/client/src/components/ListQuestionInfo.jsx
@@ -1,16 +1,24 @@
-import { Typography } from "@mui/material"
-export default function ListQuestionInfo({upvotes, downvotes, answers, views, inline}){
- return inline? (
+import { Chip, Typography } from '@mui/material';
+export default function ListQuestionInfo({
+ answers,
+ downvotes,
+ inline,
+ hasAcceptedAnswer,
+ tags,
+ upvotes,
+ views,
+}) {
+ return inline ? (
<>
- {upvotes - downvotes} votes |
- {answers} answers |
- {views} views
+ {upvotes - downvotes} votes |
+ {answers} answers |
+ {views} views
>
) : (
<>
- {upvotes - downvotes} votes
- {answers} answers
- {views} views
+ {upvotes - downvotes} votes
+ {answers} answers
+ {views} views
>
- )
-}
\ No newline at end of file
+ );
+}
diff --git a/client/src/components/MailUnit.jsx b/client/src/components/MailUnit.jsx
index d818e4d..e3b6187 100644
--- a/client/src/components/MailUnit.jsx
+++ b/client/src/components/MailUnit.jsx
@@ -3,11 +3,23 @@ import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from '
import ReactTimeAgo from 'react-time-ago';
import { Markdown } from 'components';
+import { readMail } from 'services/mailServices';
-export default function MailUnit({ sender, createdAt, subject, text }) {
+export default function MailUnit({
+ mail_id,
+ sender,
+ createdAt,
+ subject,
+ text,
+ read,
+}) {
return (
- }>
+ }
+ onClick={() => { readMail(mail_id) }}
+ sx={read && { backgroundColor: 'lightGray' }}
+ >
From {sender}
diff --git a/client/src/components/MobileNavbar.jsx b/client/src/components/MobileNavbar.jsx
index 8e9be9a..3e4f0f5 100644
--- a/client/src/components/MobileNavbar.jsx
+++ b/client/src/components/MobileNavbar.jsx
@@ -40,6 +40,16 @@ export default function MobileNavbar({ logout, userData, open, setOpen, mode, se
Dashboard
+
+
+
+ {userData?.level}
+ {userData?.points}
+ {userData?.badgeCount?.gold}
+ /{userData?.badgeCount?.silver}
+ /{userData?.badgeCount?.bronze}
+
+
diff --git a/client/src/components/PaginatedList.jsx b/client/src/components/PaginatedList.jsx
index 8534b4f..df8ad0c 100644
--- a/client/src/components/PaginatedList.jsx
+++ b/client/src/components/PaginatedList.jsx
@@ -7,7 +7,7 @@ export default function PaginatedList({ count, Component, data, handleChangePage
))}
{count > 1 && (
-
+
-
- {level} - {points}
-
+ {level}
+ {points}
+ {badgeCount?.gold}
+ /{badgeCount?.silver}
+ /{badgeCount?.bronze}
>
);
}
diff --git a/client/src/components/QAComponents/Answer.jsx b/client/src/components/QAComponents/Answer.jsx
index a1d45f1..d3b9d43 100644
--- a/client/src/components/QAComponents/Answer.jsx
+++ b/client/src/components/QAComponents/Answer.jsx
@@ -1,25 +1,28 @@
import CheckIcon from '@mui/icons-material/Check';
+import ShareIcon from '@mui/icons-material/Share';
import { Button, ButtonGroup, ListItem, ListItemText, Tooltip } from '@mui/material';
+import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Markdown } from 'components';
import { CreationInfoTag } from 'controllers';
import { AnswerCommentsList, CommentControl, VoteControl } from 'controllers/QAControllers';
export default function Answer({
- accepted,
answer_id,
+ question_id,
+ accepted,
comments,
creator,
createdAt,
downvotes,
text,
upvotes,
+ acceptAnswer,
canComment,
canAccept,
+ postComment,
getVote,
updateVote,
- postComment,
- acceptAnswer,
}) {
return (
@@ -47,12 +50,26 @@ export default function Answer({
+
+ }
+ style={{ textTransform: 'none' }}
+ variant='text'
+ >
+ Share
+
+
{canAccept && (
diff --git a/client/src/components/QAComponents/AnswerComment.jsx b/client/src/components/QAComponents/AnswerComment.jsx
index 79fabdb..c89e9a7 100644
--- a/client/src/components/QAComponents/AnswerComment.jsx
+++ b/client/src/components/QAComponents/AnswerComment.jsx
@@ -1,4 +1,5 @@
-import { Divider, ListItem, ListItemText } from '@mui/material';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { Divider, IconButton, ListItem, ListItemText } from '@mui/material';
import { CreationInfoTag } from 'controllers';
import { VoteControl } from 'controllers/QAControllers';
@@ -11,6 +12,7 @@ export default function AnswerComment({
text,
upvotes,
getVote,
+ onDelete,
updateVote,
}) {
return (
@@ -21,6 +23,9 @@ export default function AnswerComment({
{text}
+ {onDelete &&
+
+ }
diff --git a/client/src/components/QAComponents/Comment.jsx b/client/src/components/QAComponents/Comment.jsx
index 6883072..8f715c8 100644
--- a/client/src/components/QAComponents/Comment.jsx
+++ b/client/src/components/QAComponents/Comment.jsx
@@ -1,4 +1,5 @@
-import { Divider, ListItem, ListItemText } from '@mui/material';
+import DeleteIcon from '@mui/icons-material/Delete';
+import { Divider, IconButton, ListItem, ListItemText } from '@mui/material';
import { CreationInfoTag } from 'controllers';
import { VoteControl } from 'controllers/QAControllers';
@@ -11,7 +12,8 @@ export default function Comment({
text,
upvotes,
getVote,
- updateVote,
+ onDelete,
+ updateVote
}) {
return (
@@ -21,6 +23,9 @@ export default function Comment({
{text}
+ {onDelete &&
+
+ }
diff --git a/client/src/components/QAComponents/CommentControl.jsx b/client/src/components/QAComponents/CommentControl.jsx
index 43fb4e4..d8eaa8f 100644
--- a/client/src/components/QAComponents/CommentControl.jsx
+++ b/client/src/components/QAComponents/CommentControl.jsx
@@ -27,11 +27,12 @@ export default function CommentControl({
color='inherit'
disableRipple
onClick={toggleShow}
+ m={1}
size='small'
startIcon={}
style={{ textTransform: 'none' }}
variant='text'
- >
+ >
Comment
diff --git a/client/src/components/QAComponents/CreateAnswer.jsx b/client/src/components/QAComponents/CreateAnswer.jsx
index fe8dc03..8479ff6 100644
--- a/client/src/components/QAComponents/CreateAnswer.jsx
+++ b/client/src/components/QAComponents/CreateAnswer.jsx
@@ -5,7 +5,7 @@ import { AnswerForm } from 'controllers/FormControllers';
export default function CreateAnswer({ canAnswer, show, toggleShow }) {
return (
-
+
{
};
export default function Question({
+ question_id,
answers,
comments,
creator,
@@ -35,6 +39,7 @@ export default function Question({
downvotes,
hasAcceptedAnswer,
status,
+ tags,
title,
text,
upvotes,
@@ -48,8 +53,21 @@ export default function Question({
getVote,
updateVote,
postComment,
+ canBounty,
+ handleBounty,
+ hasBounty,
+ show,
}) {
+ const min = 75;
+ const max = 500;
+ const [value, setValue] = useState(75);
+ canBounty = canBounty && !hasBounty
+
+ function handleBountyFix() {
+ handleBounty(value)
+ }
return (
+
<>
{title}
@@ -75,10 +93,12 @@ export default function Question({
label={hasAcceptedAnswer ? 'yes' : 'no'}
size='small'
/>
+ Tags: {tags.join(', ')}
+
+
+
+
+
+ Add Bounty
+
+
+ {show &&
+ {
+ if (e.target.value === "") {
+ setValue(75);
+ return;
+ }
+ const value = e.target.value;
+ if (value > max) {
+ setValue(max);
+ } else if (value < min) {
+ setValue(min);
+ } else {
+ setValue(value);
+ }
+
+
+ }} />
+ }
+
+
+
{ongoingVote.users.length > 0 && (
{ongoingVote.users.toString()} - voting to {ongoingVote.type} this question{' '}
)}
+ {hasBounty && (
+
+ there is a {hasBounty} point bounty on this question
+
+ )}
+
@@ -141,6 +203,18 @@ export default function Question({
+
+ }
+ style={{ textTransform: 'none' }}
+ variant='text'
+ >
+ Share
+
+
>
diff --git a/client/src/components/QAComponents/Suggest.jsx b/client/src/components/QAComponents/Suggest.jsx
new file mode 100644
index 0000000..9e3320d
--- /dev/null
+++ b/client/src/components/QAComponents/Suggest.jsx
@@ -0,0 +1,68 @@
+import {Tooltip, Button, Card, CardContent, Typography} from '@mui/material'
+import { editQuestion } from 'services/questionsServices';
+import EditFormController from "controllers/FormControllers/EditQuestionFormController";
+import { useEffect, useState } from "react";
+import { useUser, useQuestion } from 'contexts';
+import { Markdown } from 'components';
+export default function Suggest(){
+ const [show, setShow] = useState(false);
+ const [canSuggest, setCanSuggest] = useState(false)
+ const {userData} = useUser();
+ const {questionData} = useQuestion();
+ const [ongoingEdit, setOngoingEdit] = useState({users : [], new : []})
+
+ useEffect(()=>{
+ setCanSuggest(userData.level >= 7)
+
+
+ if(!questionData.loading) setOngoingEdit({users : questionData.edit, new : questionData.editText})
+ },[userData, questionData, setCanSuggest])
+
+ async function toggleShow(){
+ if(questionData.edit.length === 0 || userData.username !== ongoingEdit.users[0]){
+ setShow(!show)
+ }else{
+ const { status } = await editQuestion(questionData.question_id);
+ window.location.reload(false);
+ }
+
+ }
+
+
+
+ return (
+
+
+
+
+ Add Suggestion
+
+
+
+ {show && (
+
+
+
+
+
+ )}
+ {ongoingEdit.users.length > 0 &&
+
+
+ New Title:
+ New Text:
+
+ {ongoingEdit.users} has voted to edit the question
+
+
+ }
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/components/index.js b/client/src/components/index.js
index 501c33f..c988981 100644
--- a/client/src/components/index.js
+++ b/client/src/components/index.js
@@ -1,3 +1,4 @@
+import Badges from './Badges';
import CreationInfoTag from './CreationInfoTag';
import Form from './Form';
import ListAnswer from './ListAnswer';
@@ -14,6 +15,7 @@ import Profile from './Profile';
import SearchBar from './SearchBar';
export {
+ Badges,
CreationInfoTag,
Form,
ListAnswer,
diff --git a/client/src/containers/Buffet.jsx b/client/src/containers/Buffet.jsx
index e6e5a4b..4eeebb1 100644
--- a/client/src/containers/Buffet.jsx
+++ b/client/src/containers/Buffet.jsx
@@ -6,30 +6,23 @@ import { ListQuestion, LoadingBar } from 'components';
import { PaginatedList } from 'controllers';
import { searchQuestions } from 'services/questionsServices';
-const recent = {};
-const best = { sort: 'u' };
-const interesting = { match: JSON.stringify({ answers: 0 }), sort: 'uvc' };
-const hot = { match: JSON.stringify({ hasAcceptedAnswer: false }), sort: 'uvac' };
-const sortObjArr = [recent, best, interesting, hot];
-
export default function Buffet() {
- const [sort, setSort] = useState(0);
+ const [sort, setSort] = useState('');
const [loading, setLoading] = useState(true);
const [, setSearchParams] = useSearchParams();
const getData = async ({ question_id }) => {
const { questions } = await searchQuestions({
- ...sortObjArr[sort],
- after: question_id,
+ sort,
});
setLoading(() => false);
return questions;
};
- const handleSortChange = (_, newSort) => {
- setSearchParams(sortObjArr[newSort]);
- setSort(newSort);
+ const handleSortChange = (_, value) => {
+ setSearchParams({ sort: value });
+ setSort(value);
};
return (
@@ -60,10 +53,10 @@ export default function Buffet() {
onChange={handleSortChange}
style={{ display: 'block', marginTop: '1%' }}
>
-
Recent
-
Best
-
Interesting
-
Hot
+
Recent
+
Best
+
Interesting
+
Hot
{loading &&
}
diff --git a/client/src/containers/Dashboard.jsx b/client/src/containers/Dashboard.jsx
index 3151eea..1a42559 100644
--- a/client/src/containers/Dashboard.jsx
+++ b/client/src/containers/Dashboard.jsx
@@ -16,7 +16,7 @@ import { Link, useNavigate } from 'react-router-dom';
import { ListAnswer, ListQuestion, LoadingBar } from 'components';
import { useUser } from 'contexts';
-import { PaginatedList } from 'controllers';
+import { Badges, PaginatedList } from 'controllers';
import { deleteUser, getUserAnswers, getUserQuestions } from 'services/userServices';
export default function Dashboard() {
@@ -25,7 +25,7 @@ export default function Dashboard() {
const [current, setCurrent] = useState('questions');
const [loading, setLoading] = useState(true);
const [confirmDelete, setConfirmDelete] = useState(false)
-
+
const {
userData: { email, level, points, username },
@@ -62,16 +62,16 @@ export default function Dashboard() {
};
const deleteCurrentUser = () => {
- if(confirmDelete){
+ if (confirmDelete) {
deleteUser();
navigate('/users/login')
setConfirmDelete(false);
- }else{
+ } else {
setConfirmDelete(true);
setTimeout(cancelDelete, 30000)
}
}
- function cancelDelete(){
+ function cancelDelete() {
setConfirmDelete(false)
}
@@ -89,18 +89,21 @@ export default function Dashboard() {
-
+
{email && (
)}
+
+
+
- {confirmDelete && Click delete button again to confirm delete, cancelling in 30 seconds}
+ {confirmDelete && Click delete button again to confirm delete, cancelling in 30 seconds}
Username: {username}
Email: {email}
Level: {level}
@@ -127,7 +130,7 @@ export default function Dashboard() {
-
+
+
+
);
diff --git a/client/src/contexts/UserContext.js b/client/src/contexts/UserContext.js
index 5baf70c..d4ac10a 100644
--- a/client/src/contexts/UserContext.js
+++ b/client/src/contexts/UserContext.js
@@ -11,9 +11,6 @@ export default function UserProvider({ children }) {
const [userData, setUserData] = useState(initialUserData)
const navigate = useNavigate();
-
-
-
useEffect(() => {
const loadUserData = async () => {
const { user, error } = await remember();
diff --git a/client/src/controllers/BadgesController.js b/client/src/controllers/BadgesController.js
new file mode 100644
index 0000000..4ab60f8
--- /dev/null
+++ b/client/src/controllers/BadgesController.js
@@ -0,0 +1,20 @@
+import { useEffect, useState } from 'react';
+
+import { Badges } from 'components';
+import { getUserBadges } from 'services/userServices';
+
+export default function BadgesController() {
+ const [badges, setBadges] = useState();
+
+ useEffect(() => {
+ const loadBadges = async () => {
+ const { badges: newBadges } = await getUserBadges();
+ setBadges(newBadges);
+ };
+ loadBadges();
+ }, []);
+
+ return (
+ badges && Badges({ badges })
+ );
+}
diff --git a/client/src/controllers/FormControllers/EditQuestionFormController.js b/client/src/controllers/FormControllers/EditQuestionFormController.js
new file mode 100644
index 0000000..3405fa9
--- /dev/null
+++ b/client/src/controllers/FormControllers/EditQuestionFormController.js
@@ -0,0 +1,27 @@
+
+import { Form } from 'controllers/FormControllers';
+import { generateEditFields } from 'services/fields';
+import { editQuestion } from 'services/questionsServices';
+import { editSchema } from 'services/schemas';
+import { useQuestion } from 'contexts';
+
+export default function EditQuestionFormController() {
+ const {questionData} = useQuestion();
+ const {title, text} = questionData;
+
+
+
+ const editTheQuestion = async (fields) => {
+ const { status } = await editQuestion(questionData.question_id, fields);
+ window.location.reload(false);
+ };
+
+ const field = generateEditFields
+
+ return !questionData.loading && Form({
+ fields: field,
+ onSubmit: editTheQuestion,
+ validationSchema: editSchema,
+ initialValues: {etext : text, etitle : title}
+ });
+}
diff --git a/client/src/controllers/FormControllers/FormController.js b/client/src/controllers/FormControllers/FormController.js
index 9d2dcc3..e6e22af 100644
--- a/client/src/controllers/FormControllers/FormController.js
+++ b/client/src/controllers/FormControllers/FormController.js
@@ -1,6 +1,5 @@
import { useFormik } from 'formik';
import { useEffect } from 'react';
-
import { Form } from 'components';
import { useForm } from 'contexts';
@@ -10,11 +9,15 @@ export default function FormController({
validate,
validationSchema,
children,
+ initialValues = {}
}) {
+
+
const { setContent } = useForm();
useEffect(() => {
setContent('');
+
}, [setContent]);
const handleChange = (e) => {
@@ -22,7 +25,7 @@ export default function FormController({
};
const formik = useFormik({
- initialValues: fields.reduce((acc, { id }) => ({ ...acc, [id]: '' }), {}),
+ initialValues: fields.reduce((acc, { id }) => ({ ...acc, [id]: initialValues[id] || '' }), {}),
onSubmit,
validate,
validateOnChange: false,
diff --git a/client/src/controllers/PaginatedListController.jsx b/client/src/controllers/PaginatedListController.jsx
index 4fdb6e0..bdf0361 100644
--- a/client/src/controllers/PaginatedListController.jsx
+++ b/client/src/controllers/PaginatedListController.jsx
@@ -8,6 +8,7 @@ export default function PaginatedListController({
count,
Component,
getData,
+ scroll,
rowsPerPage = 5,
}) {
const [searchParams] = useSearchParams();
@@ -25,7 +26,19 @@ export default function PaginatedListController({
const newData = await getData(clear ? {} : data[data.length - 1] ?? {});
if (newData?.length)
- if (clear) setData(newData);
+ if (clear) {
+ setData(newData);
+ if (scroll) {
+ setTimeout(() => {
+ const currentLocation = window.location.href;
+ const hasAnswerAnchor = currentLocation.includes('#');
+ if (hasAnswerAnchor) {
+ const anchorAnswer = document.getElementById('answersList');
+ anchorAnswer.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, 2000)
+ }
+ }
else setData(data.concat(newData));
else setLoad(false);
};
diff --git a/client/src/controllers/QAControllers/AnswerCommentController.jsx b/client/src/controllers/QAControllers/AnswerCommentController.jsx
index 3c9c4d6..b83c48a 100644
--- a/client/src/controllers/QAControllers/AnswerCommentController.jsx
+++ b/client/src/controllers/QAControllers/AnswerCommentController.jsx
@@ -1,9 +1,19 @@
+import { useUser } from 'contexts';
import { AnswerComment } from 'components/QAComponents';
-import { getAnswerCommentVote, updateAnswerCommentVote } from 'services/questionsServices';
+import { getAnswerCommentVote, deleteAnswerComment, updateAnswerCommentVote } from 'services/questionsServices';
export default function AnswerCommentController({ answer_id, comment_id, question_id, ...props }) {
+ const { userData } = useUser();
+
const getVote = () => getAnswerCommentVote(question_id, answer_id, comment_id);
const updateVote = (data) => updateAnswerCommentVote(question_id, answer_id, comment_id, data);
+ const onDelete = async () => {
+ if (userData?.username === props?.creator) {
+ const { status } = await deleteAnswerComment(question_id, answer_id, comment_id);
+ if (status === 200)
+ window.location.reload(false);
+ }
+ }
- return ;
+ return ;
}
diff --git a/client/src/controllers/QAControllers/AnswerController.jsx b/client/src/controllers/QAControllers/AnswerController.jsx
index 62aebdb..fe09960 100644
--- a/client/src/controllers/QAControllers/AnswerController.jsx
+++ b/client/src/controllers/QAControllers/AnswerController.jsx
@@ -7,12 +7,10 @@ import {
} from 'services/questionsServices';
export default function AnswerController({ answer_id, question_id, ...props }) {
+ const acceptAnswer = () => updateAcceptAnswer(question_id, answer_id);
const getVote = () => getAnswerVote(question_id, answer_id);
const updateVote = (data) => updateAnswerVote(question_id, answer_id, data);
- const postComment = async (data) => {
- await postAnswerComment(question_id, answer_id, data);
- };
- const acceptAnswer = () => updateAcceptAnswer(question_id, answer_id);
+ const postComment = async (data) => { await postAnswerComment(question_id, answer_id, data); };
return (
);
diff --git a/client/src/controllers/QAControllers/CommentController.jsx b/client/src/controllers/QAControllers/CommentController.jsx
index 87047fe..0e22cc7 100644
--- a/client/src/controllers/QAControllers/CommentController.jsx
+++ b/client/src/controllers/QAControllers/CommentController.jsx
@@ -1,9 +1,20 @@
+import { useUser } from 'contexts';
import { Comment } from 'components/QAComponents';
-import { getCommentVote, updateCommentVote } from 'services/questionsServices';
+import { getCommentVote, deleteQuestionComment, updateCommentVote } from 'services/questionsServices';
+
export default function CommentController({ comment_id, question_id, ...props }) {
+ const { userData } = useUser();
+
const getVote = () => getCommentVote(question_id, comment_id);
const updateVote = (data) => updateCommentVote(question_id, comment_id, data);
+ const onDelete = async () => {
+ if (userData?.username === props?.creator) {
+ const { status } = await deleteQuestionComment(question_id, comment_id);
+ if (status === 200)
+ window.location.reload(false);
+ }
+ }
- return ;
+ return ;
}
diff --git a/client/src/controllers/QAControllers/QAPaginatedListController.jsx b/client/src/controllers/QAControllers/QAPaginatedListController.jsx
index 177b63b..f6382b5 100644
--- a/client/src/controllers/QAControllers/QAPaginatedListController.jsx
+++ b/client/src/controllers/QAControllers/QAPaginatedListController.jsx
@@ -27,16 +27,33 @@ export function AnswersList() {
const sortByPoints = (answers) =>
answers.sort((a, b) => b.upvotes - b.downvotes - a.upvotes + a.downvotes);
- const getData = ({ answer_id }) =>
- getAnswers(question_id, { after: answer_id })
+ const getData = async ({ answer_id }) => {
+ const answersList = await getAnswers(question_id, { after: answer_id })
.then(({ answers }) =>
sortByPoints(answers).map((answer) => ({ ...answer, question_id }))
)
.catch(() => []);
+ const currentLocation = window.location.href;
+ const hasAnswerAnchor = currentLocation.includes('#');
+
+ if (hasAnswerAnchor) {
+ const anchorAnswerId = `${currentLocation.substring(currentLocation.indexOf("#") + 1)}`;
+ answersList.forEach(function (answer, i) {
+ if (answer.answer_id === anchorAnswerId) {
+ answersList.splice(i, 1);
+ answersList.unshift(answer);
+ }
+ });
+ }
+ if (answersList.length > 2 && answersList[0].answer_id === answersList[1].answer_id)
+ answersList.splice(0, 1);
+
+ return answersList;
+ }
const NewAnswer = (props) => ;
- return !loading && ;
+ return !loading && ;
}
export function CommentsList() {
diff --git a/client/src/controllers/QAControllers/QuestionController.jsx b/client/src/controllers/QAControllers/QuestionController.jsx
index e27afad..4a0d0aa 100644
--- a/client/src/controllers/QAControllers/QuestionController.jsx
+++ b/client/src/controllers/QAControllers/QuestionController.jsx
@@ -4,7 +4,9 @@ import { Question } from 'components/QAComponents';
import { useQuestion, useUser } from 'contexts';
import { useNavigate, useParams } from 'react-router-dom';
import getPermissions from 'services/getPermissions';
+
import {
+ addBounty,
closeQuestion,
getQuestionVote,
openQuestion,
@@ -13,17 +15,18 @@ import {
updateQuestion,
updateQuestionVote,
} from 'services/questionsServices';
+import { SettingsPhoneTwoTone } from '@mui/icons-material';
export default function QuestionController() {
const navigate = useNavigate();
-
+ let canBounty = false;
const { question_id } = useParams();
const { questionData } = useQuestion();
const { protect, close, reopen, status } = questionData;
const { userData } = useUser();
const [ongoingVote, setOngoingVote] = useState({ users: [], type: 'none' });
const { permissions, setPermissions } = useQuestion();
-
+ const [show, setShow] = useState(false)
function setVote() {
if (!questionData.loading) {
if (protect.length) setOngoingVote({ users: protect, type: 'protect' });
@@ -34,7 +37,9 @@ export default function QuestionController() {
useEffect(() => {
const permissions = getPermissions(questionData, userData, ongoingVote);
+ if (userData.username) canBounty = (userData.level >= 4 && status !== 'closed' && !questionData.hasBounty && !questionData.hasAcceptedAnswer)
+ permissions.canBounty = canBounty;
setPermissions(permissions);
setVote();
}, [userData, questionData]);
@@ -55,6 +60,7 @@ export default function QuestionController() {
? protect.splice(protect.indexOf(userData.username), 1)
: protect.push(userData.username);
ongoingVoteSet(protect, 'protect');
+ window.location.reload(false);
}
function changeClose() {
@@ -64,12 +70,25 @@ export default function QuestionController() {
? close.splice(close.indexOf(userData.username), 1)
: close.push(userData.username);
ongoingVoteSet(close, 'close');
+ window.location.reload(false);
} else {
openQuestion(question_id, userData);
reopen.includes(userData.username)
? reopen.splice(reopen.indexOf(userData.username), 1)
: reopen.push(userData.username);
ongoingVoteSet(reopen, 'reopen');
+ window.location.reload(false);
+ }
+ }
+ function handleBounty(amount) {
+ if (show) {
+ const data = {amount: amount}
+ addBounty(question_id, data)
+ questionData.hasBounty = amount;
+ userData.points -= amount;
+ setShow(false)
+ } else {
+ setShow(true)
}
}
@@ -93,6 +112,9 @@ export default function QuestionController() {
getVote,
updateVote,
postComment,
+ show,
+ handleBounty
+
}}
/>
)}
diff --git a/client/src/controllers/SearchBarController.js b/client/src/controllers/SearchBarController.js
index 2202691..b01a111 100644
--- a/client/src/controllers/SearchBarController.js
+++ b/client/src/controllers/SearchBarController.js
@@ -15,12 +15,12 @@ export default function SearchBarController() {
e.preventDefault();
if (location.pathname === '/questions/search') {
- setSearchParams({ title: search, text: search, creator: search }, { replace: true });
+ setSearchParams({ title: search }, { replace: true });
} else {
navigate({
pathname: '/questions/search',
search: `?${createSearchParams({
- title: search, text: search, creator: search
+ title: search,
})}`,
replace: true,
});
diff --git a/client/src/controllers/SearchResultsController.jsx b/client/src/controllers/SearchResultsController.jsx
index 196821f..85fe890 100644
--- a/client/src/controllers/SearchResultsController.jsx
+++ b/client/src/controllers/SearchResultsController.jsx
@@ -9,31 +9,26 @@ export default function SearchResultsController() {
const [searchParams] = useSearchParams();
const [loading, setLoading] = useState(true);
- const getData = async ({ question_id }) => {
- let match = {};
- const regexMatch = {};
- const time = {};
- let newdate = searchParams.get('createdAt');
+ const getData = async () => {
+ const creator = searchParams.get('creator');
+ const title = searchParams.get('title');
+ const text = searchParams.get('text');
+ let tags = searchParams.get('tags');
+ let time = searchParams.get('createdAt');
+ let createdAt = {};
- if (newdate) {
- newdate = new Date(newdate);
- time['$gte'] = parseInt(newdate.getTime());
- newdate.setDate(newdate.getDate() + 1);
- time['$lte'] = newdate.getTime();
- match = JSON.stringify({ createdAt: time });
+ if (time) {
+ time = new Date(time);
+ createdAt['$gte'] = parseInt(time.getTime());
+ time.setDate(time.getDate() + 1);
+ createdAt['$lte'] = time.getTime();
+ }
+ if (tags) {
+ tags = tags.split(' ');
}
- const creator = searchParams.get('creator');
- if (creator) regexMatch.creator = creator;
-
- const text = searchParams.get('text');
- if (text) regexMatch.text = text.replaceAll(' ', '|');
-
- const title = searchParams.get('title');
- if (title) regexMatch.title = title.replaceAll(' ', '|');
-
setLoading(() => true);
- const { questions } = await searchQuestions({ after: question_id, regexMatch, match });
+ const { questions } = await searchQuestions({ creator, title, text, tags, createdAt });
setLoading(() => false);
return questions;
diff --git a/client/src/controllers/index.js b/client/src/controllers/index.js
index c538de9..6df7eee 100644
--- a/client/src/controllers/index.js
+++ b/client/src/controllers/index.js
@@ -1,3 +1,4 @@
+import Badges from './BadgesController';
import CreationInfoTag from './CreationInfoTagController';
import Inbox from './InboxController';
import MdPreview from './MdPreviewController';
@@ -6,4 +7,4 @@ import PaginatedList from './PaginatedListController';
import SearchBar from './SearchBarController';
import SearchResults from './SearchResultsController';
-export { CreationInfoTag, Inbox, MdPreview, Navbar, PaginatedList, SearchBar, SearchResults };
+export { Badges, CreationInfoTag, Inbox, MdPreview, Navbar, PaginatedList, SearchBar, SearchResults };
diff --git a/client/src/services/fields.js b/client/src/services/fields.js
index 5a8295e..e99386f 100644
--- a/client/src/services/fields.js
+++ b/client/src/services/fields.js
@@ -22,6 +22,7 @@ const askQuestionFields = [
label: 'Text',
multiline: true,
},
+ { id: 'tags', label: 'Tags' },
];
const commentFields = [
@@ -127,8 +128,25 @@ const searchFields = [
id: 'creator',
label: 'Creator',
},
+ { id: 'tags', label: 'Tags (separated by spaces)' },
];
+const generateEditFields =
+
+ [
+ {
+ id: 'etitle',
+ label: 'Title',
+
+ },
+ {
+ id: 'etext',
+ label: 'Text',
+ multiline: true,
+
+ },
+ ]
+
export {
answerFields,
askQuestionFields,
@@ -140,4 +158,5 @@ export {
recoverFields,
resetFields,
searchFields,
+ generateEditFields
};
diff --git a/client/src/services/getPermissions.js b/client/src/services/getPermissions.js
index e9e6eab..fd325b1 100644
--- a/client/src/services/getPermissions.js
+++ b/client/src/services/getPermissions.js
@@ -1,5 +1,7 @@
+
function getPermissions(questionData, userData, ongoingVote) {
- const { creator, status, hasAcceptedAnswer } = questionData;
+
+ const { creator, status, hasAcceptedAnswer, close, reopen, protect } = questionData;
const { username } = userData;
var permissions = {
@@ -10,19 +12,24 @@ function getPermissions(questionData, userData, ongoingVote) {
canAccept: false,
};
+
const level = userData.level || 0;
const protection = status === 'protected' || status === 'closed';
- if (username) {
+ if (username && status) {
+ const ongoingProtect = protect.length > 0;
+ const ongoingClose = close.length > 0;
+ const ongoingOpen= reopen.length > 0;
+
if (userData.username === creator && !hasAcceptedAnswer) {
permissions.canAccept = true;
}
- if (level >= 7 && ongoingVote.type !== 'protect') {
+ if (level >= 7 && ongoingVote.type !== 'protect' && !ongoingProtect) {
permissions.canClose = true;
}
- if (level >= 6 && ongoingVote.type !== 'close' && !protection) {
+ if (level >= 6 && ongoingVote.type !== 'close' && !protection && !ongoingClose) {
permissions.canProtect = true;
}
diff --git a/client/src/services/levels.json b/client/src/services/levels.json
index 67b2b4f..f6cb681 100644
--- a/client/src/services/levels.json
+++ b/client/src/services/levels.json
@@ -1,8 +1,11 @@
{
+ "Level 1": 1,
"Level 2": 15,
"Level 3": 50,
- "Level 4": 125,
- "Level 5": 1000,
- "Level 6": 3000,
- "Level 7": 10000
+ "Level 4": 75,
+ "Level 5": 125,
+ "Level 6": 1000,
+ "Level 7": 2000,
+ "Level 8": 3000,
+ "Level 9": 10000
}
diff --git a/client/src/services/mailServices.js b/client/src/services/mailServices.js
index 2dabea8..becd381 100644
--- a/client/src/services/mailServices.js
+++ b/client/src/services/mailServices.js
@@ -2,8 +2,11 @@ import { createEndpoint } from './api';
const callMailAPI = createEndpoint('/mail');
-const postMail = async (data) => callMailAPI('post', ``, data);
-
const getMail = async () => callMailAPI('get', ``);
-export { getMail, postMail };
+const postMail = async (data) => callMailAPI('post', ``, data);
+
+const readMail = async (mail_id) => callMailAPI('patch', `/${mail_id}`, { read: 'true' });
+
+
+export { getMail, postMail, readMail };
diff --git a/client/src/services/questionsServices.js b/client/src/services/questionsServices.js
index f10b55d..6c09f9c 100644
--- a/client/src/services/questionsServices.js
+++ b/client/src/services/questionsServices.js
@@ -8,6 +8,18 @@ const postQuestion = async (data) => callQuestionsAPI('post', ``, data);
const getQuestion = async (question_id) => callQuestionsAPI('get', `/${question_id}`);
+const addBounty = async (question_id, data) => callQuestionsAPI(
+ 'patch',
+ `/${question_id}/addBounty`,
+ data
+
+)
+const editQuestion = async (question_id, data) => callQuestionsAPI(
+ 'patch',
+ `/${question_id}/edit`,
+ data
+)
+
const updateQuestion = async (question_id, data) =>
callQuestionsAPI('patch', `/${question_id}`, data);
@@ -103,4 +115,6 @@ export {
closeQuestion,
openQuestion,
protectQuestion,
+ addBounty,
+ editQuestion
};
diff --git a/client/src/services/schemas.js b/client/src/services/schemas.js
index f232e96..9abbee0 100644
--- a/client/src/services/schemas.js
+++ b/client/src/services/schemas.js
@@ -21,6 +21,10 @@ const searchSchema = Yup.object({
text: Yup.string().max(300, 'Text must be at most 300 characters, try and use keywords only '),
// createdAt: Yup.string().max(11, 'Date must be at most 11 characters, make sure body is formatted correctly'),
creator: Yup.string().max(16, 'username must be at most 16 characters'),
+ tags: Yup.string().matches(
+ /^(?:\b\w+\b[\s\r\n]*){0,5}$/,
+ 'Tags should be separated by spaces and are limited to 5'
+ ),
});
const loginSchema = Yup.object({
@@ -71,6 +75,10 @@ const questionSchema = Yup.object({
text: Yup.string()
.max(3000, 'Body cannot be longer than 3000 characters')
.required('A body is required'),
+ tags: Yup.string().matches(
+ /^(?:\b\w+\b[\s\r\n]*){0,5}$/,
+ 'Tags should be separated by spaces and are limited to 5'
+ ),
});
const patchSchema = Yup.object().shape(
@@ -99,6 +107,14 @@ const patchSchema = Yup.object().shape(
['password', 'password'],
]
);
+const editSchema = Yup.object({
+ etitle: Yup.string()
+ .max(150, 'Title cannot be longer then 150 characters.')
+ .required('A title is required.'),
+ etext: Yup.string()
+ .max(3000, 'Body cannot be longer than 3000 characters')
+ .required('A body is required')
+});
export {
answerSchema,
@@ -111,4 +127,5 @@ export {
registerSchema,
resetSchema,
searchSchema,
+ editSchema
};
diff --git a/client/src/services/userServices.js b/client/src/services/userServices.js
index 3553702..7e9f04f 100644
--- a/client/src/services/userServices.js
+++ b/client/src/services/userServices.js
@@ -6,47 +6,49 @@ import { createEndpoint } from './api';
const callUsersAPI = createEndpoint('/users');
const getLoginAttempts = () => {
- let loginAttempts = Number(Cookies.get('loginAttempts')) || 0;
- let loginTimeout = Number(Cookies.get('loginTimeout')) - Date.now() || 0;
+ let loginAttempts = Number(Cookies.get('loginAttempts')) || 0;
+ let loginTimeout = Number(Cookies.get('loginTimeout')) - Date.now() || 0;
- if (loginTimeout < 0) {
- Cookies.remove('loginAttempts');
- Cookies.remove('loginTimeout');
- loginAttempts = 0;
- loginTimeout = 0;
- }
+ if (loginTimeout < 0) {
+ Cookies.remove('loginAttempts');
+ Cookies.remove('loginTimeout');
+ loginAttempts = 0;
+ loginTimeout = 0;
+ }
- return { loginAttempts, loginTimeout };
+ return { loginAttempts, loginTimeout };
};
const getUser = async (username) => callUsersAPI('get', `/${username}`);
-const getUserQuestions = async () => callUsersAPI('get', `/questions`);
-
const getUserAnswers = async () => callUsersAPI('get', `/answers`);
-const deleteUser = async () => callUsersAPI('delete',`/delete`)
+const getUserBadges = async () => callUsersAPI('get', `/badges`);
+
+const getUserQuestions = async () => callUsersAPI('get', `/questions`);
+
+const deleteUser = async () => callUsersAPI('delete', `/delete`)
const incrementLoginAttempts = () => {
- const { loginAttempts } = getLoginAttempts();
- Cookies.set('loginAttempts', loginAttempts + 1);
- if (loginAttempts >= 2) Cookies.set('loginTimeout', Date.now() + 1000 * 60 * 5);
+ const { loginAttempts } = getLoginAttempts();
+ Cookies.set('loginAttempts', loginAttempts + 1);
+ if (loginAttempts >= 2) Cookies.set('loginTimeout', Date.now() + 1000 * 60 * 5);
};
const login = async ({ username, password }) => {
- const encoded = Buffer.from(`${username}:${password}`).toString('base64');
- const { token, ...data } = await callUsersAPI(
- 'post',
- `/login`,
- { remember: false },
- `basic ${encoded}`
- );
- if (token) {
- Cookies.set('token', token);
- Cookies.remove('loginAttempts');
- Cookies.remove('loginTimeout');
- }
- return data;
+ const encoded = Buffer.from(`${username}:${password}`).toString('base64');
+ const { token, ...data } = await callUsersAPI(
+ 'post',
+ `/login`,
+ { remember: false },
+ `basic ${encoded}`
+ );
+ if (token) {
+ Cookies.set('token', token);
+ Cookies.remove('loginAttempts');
+ Cookies.remove('loginTimeout');
+ }
+ return data;
};
const logout = async () => callUsersAPI('post', `/login`, { remember: false });
@@ -56,13 +58,13 @@ const updateUser = async (data) => callUsersAPI('patch', ``, data);
const register = async (data) => callUsersAPI('post', ``, data);
const remember = async () => {
- if (Cookies.get('token')) {
- const { user, error } = await callUsersAPI('get', `/remember`);
- if (user)
- return { user };
- Cookies.remove('token');
- return { error }
- }
+ if (Cookies.get('token')) {
+ const { user, error } = await callUsersAPI('get', `/remember`);
+ if (user)
+ return { user };
+ Cookies.remove('token');
+ return { error }
+ }
}
const requestReset = async (data) => callUsersAPI('post', `/reset`, data);
@@ -70,17 +72,18 @@ const requestReset = async (data) => callUsersAPI('post', `/reset`, data);
const resetPassword = async (id, data) => callUsersAPI('post', `/reset/${id}`, data);
export {
- getLoginAttempts,
- getUser,
- getUserAnswers,
- getUserQuestions,
- incrementLoginAttempts,
- login,
- logout,
- register,
- remember,
- updateUser,
- requestReset,
- resetPassword,
- deleteUser
+ getLoginAttempts,
+ getUser,
+ getUserAnswers,
+ getUserBadges,
+ getUserQuestions,
+ incrementLoginAttempts,
+ login,
+ logout,
+ register,
+ remember,
+ updateUser,
+ requestReset,
+ resetPassword,
+ deleteUser
};
diff --git a/package-lock.json b/package-lock.json
index 7c2964e..f03b82c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,9 @@
"client",
"server"
],
+ "dependencies": {
+ "react-copy-to-clipboard": "^5.1.0"
+ },
"devDependencies": {
"concurrently": "^7.3.0"
}
@@ -32,6 +35,7 @@
"js-cookie": "^3.0.1",
"marked": "^4.0.18",
"react": "^18.2.0",
+ "react-copy-to-clipboard": "^5.1.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
@@ -6566,6 +6570,14 @@
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
},
+ "node_modules/copy-to-clipboard": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
+ "integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
"node_modules/core-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
@@ -16545,6 +16557,18 @@
"node": ">=14"
}
},
+ "node_modules/react-copy-to-clipboard": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
+ "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
+ "dependencies": {
+ "copy-to-clipboard": "^3.3.1",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": "^15.3.0 || 16 || 17 || 18"
+ }
+ },
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -18943,6 +18967,11 @@
"node": ">=8.0"
}
},
+ "node_modules/toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -25205,6 +25234,14 @@
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
},
+ "copy-to-clipboard": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
+ "integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
+ "requires": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
"core-js": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
@@ -32141,6 +32178,7 @@
"js-cookie": "^3.0.1",
"marked": "^4.0.18",
"react": "^18.2.0",
+ "react-copy-to-clipboard": "^5.1.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
@@ -32233,6 +32271,15 @@
"whatwg-fetch": "^3.6.2"
}
},
+ "react-copy-to-clipboard": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
+ "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
+ "requires": {
+ "copy-to-clipboard": "^3.3.1",
+ "prop-types": "^15.8.1"
+ }
+ },
"react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@@ -34023,6 +34070,11 @@
"is-number": "^7.0.0"
}
},
+ "toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
+ },
"toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
diff --git a/package.json b/package.json
index 98f570a..1b66d94 100644
--- a/package.json
+++ b/package.json
@@ -30,5 +30,8 @@
],
"devDependencies": {
"concurrently": "^7.3.0"
+ },
+ "dependencies": {
+ "react-copy-to-clipboard": "^5.1.0"
}
}
diff --git a/server/badges.json b/server/badges.json
new file mode 100644
index 0000000..ddeb065
--- /dev/null
+++ b/server/badges.json
@@ -0,0 +1,64 @@
+{
+ "badges": [
+ {
+ "title": "Great Question",
+ "text": "Have a question with net total points of >100",
+ "rank": "Gold"
+ },
+ {
+ "title": "Good Question",
+ "text": "Have a question with net total points of >25",
+ "rank": "Silver"
+ },
+ {
+ "title": "Nice Question",
+ "text": "Have a question with net total points of >10",
+ "rank": "Bronze"
+ },
+ {
+ "title": "Great Answer",
+ "text": "Have a answer with net total points of >100",
+ "rank": "Gold"
+ },
+ {
+ "title": "Good Answer",
+ "text": "Have a answer with net total points of >25",
+ "rank": "Silver"
+ },
+ {
+ "title": "Nice Answer",
+ "text": "Have a answer with net total points of >10",
+ "rank": "Bronze"
+ },
+ {
+ "title": "Socratic",
+ "text": "Have at least 10000 points",
+ "rank": "Gold"
+ },
+ {
+ "title": "Inquisitive",
+ "text": "Have at least 3000 points",
+ "rank": "Silver"
+ },
+ {
+ "title": "Curious",
+ "text": "Have at least 100 points",
+ "rank": "Bronze"
+ },
+ {
+ "title": "Zombie",
+ "text": "Have a question that is reopened",
+ "rank": "Gold"
+ },
+ {
+ "title": "Protected",
+ "text": "Have a question that is protected",
+ "rank": "Silver"
+ },
+ {
+ "title": "Scholar",
+ "text": "Accept an answer",
+ "rank": "Bronze"
+ }
+ ]
+}
diff --git a/server/bin/www b/server/bin/www
index d0655af..1ba6662 100644
--- a/server/bin/www
+++ b/server/bin/www
@@ -43,8 +43,8 @@ mongoose.connection.on(
console.error.bind(console, '[ERROR]: MongoDB connection error - ')
);
-//cron.schedule('*/30 * * * * *', refreshQuestions);
-//cron.schedule('*/2 * * * *', refreshUsers);
+// cron.schedule('*/30 * * * * *', refreshQuestions);
+// cron.schedule('*/2 * * * *', refreshUsers);
server.listen(port, console.log(`Listening on port: ${port}`));
server.on('error', onError);
diff --git a/server/db/models/Answer.js b/server/db/models/Answer.js
index 7519a43..f705a31 100644
--- a/server/db/models/Answer.js
+++ b/server/db/models/Answer.js
@@ -1,4 +1,6 @@
const mongoose = require('mongoose');
+const calculateAnswerBadges = require('../../utils/badges/answerBadges');
+const User = require('./User');
const Answer = mongoose.Schema(
{
@@ -15,4 +17,11 @@ const Answer = mongoose.Schema(
{ timestamps: { createdAt: false, updatedAt: true } }
);
+Answer.post('findOneAndUpdate', (doc) => {
+ if (doc) {
+ const badges = calculateAnswerBadges(doc.upvotes - doc.downvotes);
+ User.findOneAndUpdate({ username: doc.creator }, { $addToSet: { tags: { $each: badges } } });
+ }
+});
+
module.exports = mongoose.model('Answer', Answer);
diff --git a/server/db/models/Badge.js b/server/db/models/Badge.js
new file mode 100644
index 0000000..7076d4a
--- /dev/null
+++ b/server/db/models/Badge.js
@@ -0,0 +1,9 @@
+const mongoose = require('mongoose');
+
+const Badge = mongoose.Schema({
+ title: { type: String, required: true },
+ text: { type: String, required: true },
+ rank: { type: String, required: true, enum: ['Gold', 'Silver', 'Bronze'] },
+});
+
+module.exports = mongoose.model('Badge', Badge);
diff --git a/server/db/models/Mail.js b/server/db/models/Mail.js
index e950d31..ac19be9 100644
--- a/server/db/models/Mail.js
+++ b/server/db/models/Mail.js
@@ -8,6 +8,7 @@ const Mail = mongoose.Schema(
sender: { type: String, required: true },
subject: { type: String, required: true },
text: { type: String, required: true },
+ read: { type: Boolean, required: true, default: false },
},
{ timestamps: false }
);
diff --git a/server/db/models/Question.js b/server/db/models/Question.js
index af65876..fbed167 100644
--- a/server/db/models/Question.js
+++ b/server/db/models/Question.js
@@ -1,9 +1,10 @@
const mongoose = require('mongoose');
+const calculateQuestionBadges = require('../../utils/badges/questionBadges');
+const User = require('./User');
const Question = mongoose.Schema(
{
answers: { type: Number, required: true, default: 0 },
- close: [String],
comments: { type: Number, required: true, default: 0 },
createdAt: { type: Date, required: true, default: Date.now },
creator: { type: String, required: true },
@@ -12,15 +13,27 @@ const Question = mongoose.Schema(
lastAnswerFetch: { type: Date, default: new Date(0) },
lastCommentFetch: { type: Date, default: new Date(0) },
protect: [String],
- question_id: { type: String, required: true },
reopen: [String],
+ close: [String],
+ edit: [String],
+ editText: [String],
+ tags: [String],
+ question_id: { type: String, required: true },
status: { type: String, required: true, default: 'open' },
text: { type: String, required: true },
title: { type: String, required: true },
upvotes: { type: Number, required: true, default: 0 },
views: { type: Number, required: true, default: 0 },
+ hasBounty: { type: Number, required: false },
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
+Question.post('findOneAndUpdate', (doc) => {
+ if(doc){
+ const badges = calculateQuestionBadges(doc.upvotes - doc.downvotes);
+ User.findOneAndUpdate({ username: doc.creator }, { $addToSet: { tags: { $each: badges } } });
+ }
+});
+
module.exports = mongoose.model('Question', Question);
diff --git a/server/db/models/User.js b/server/db/models/User.js
index 5a4124c..3eb6861 100644
--- a/server/db/models/User.js
+++ b/server/db/models/User.js
@@ -1,4 +1,5 @@
const mongoose = require('mongoose');
+const calculateUserBadges = require('server/utils/badges/userBadges');
const User = mongoose.Schema(
{
@@ -9,8 +10,15 @@ const User = mongoose.Schema(
lastMailFetch: { type: Date, default: new Date(0) },
lastAnswerFetch: { type: Date, default: new Date(0) },
user_id: { type: String, required: true, unique: true },
+ badges: [String],
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
+User.post('findOneAndUpdate', async (doc) => {
+ const badges = calculateUserBadges(doc.points);
+ doc.badges = [...new Set([...doc.badges, ...badges])];
+ await doc.save();
+});
+
module.exports = mongoose.model('User', User);
diff --git a/server/routes/mail.js b/server/routes/mail.js
index fc8570c..faa87ef 100644
--- a/server/routes/mail.js
+++ b/server/routes/mail.js
@@ -5,8 +5,10 @@ const tokenAuth = require('server/middleware/tokenAuth');
const GetMail = require('./mail/Get');
const SendMail = require('./mail/Send');
+const ReadMail = require('./mail/Read');
router.get('/', tokenAuth, GetMail);
router.post('/', tokenAuth, SendMail);
+router.patch('/:mail_id', tokenAuth, ReadMail);
module.exports = router;
diff --git a/server/routes/mail/Get.js b/server/routes/mail/Get.js
index 81e0dc4..861c56f 100644
--- a/server/routes/mail/Get.js
+++ b/server/routes/mail/Get.js
@@ -7,15 +7,15 @@ async function Get(req, res) {
// Fetch mail if expired
if (Number(lastMailFetch) + config.mailExpires < Date.now()) {
- const messages = await getAllMail({ username });
- return res.send({ messages });
- } else {
+
+ await getAllMail({ username })
+ }
const messages = await Mail.find({ receiver: username }).sort({
createdAt: 'desc',
});
return res.send({ messages });
- }
+
}
module.exports = Get;
diff --git a/server/routes/mail/Read.js b/server/routes/mail/Read.js
new file mode 100644
index 0000000..c2003a3
--- /dev/null
+++ b/server/routes/mail/Read.js
@@ -0,0 +1,12 @@
+const Mail = require('server/db/models/Mail');
+const config = require('server/config.json');
+
+async function Read(req, res) {
+ const { mail_id } = req.params;
+
+ const cachedMail = await Mail.findByIdAndUpdate(mail_id, { read: true });
+
+ return cachedMail ? res.sendStatus(200) : res.status(404).send(config.errorNotFound);
+}
+
+module.exports = Read;
diff --git a/server/routes/question.js b/server/routes/question.js
index 1b07e60..81b2cf7 100644
--- a/server/routes/question.js
+++ b/server/routes/question.js
@@ -3,7 +3,6 @@ const router = express.Router();
const tokenAuth = require('server/middleware/tokenAuth');
-
const CreateAnswer = require('./question/CreateAnswer');
const CreateAnswerComment = require('./question/CreateAnswerComment');
const CreateComment = require('./question/CreateComment');
@@ -16,6 +15,8 @@ const EditAnswerCommentVote = require('./question/EditAnswerCommentVote');
const EditAnswerVote = require('./question/EditAnswerVote');
const EditCommentVote = require('./question/EditCommentVote');
const EditQuestion = require('./question/EditQuestion');
+const EditQuestionBody = require('./question/EditQuestionBody');
+const EditQuestionBounty = require('./question/EditQuestionBounty');
const EditQuestionStatusClosed = require('./question/EditQuestionStatusClosed');
const EditQuestionStatusProtected = require('./question/EditQuestionStatusProtected');
const EditQuestionStatusReopened = require('./question/EditQuestionStatusReopened');
@@ -31,10 +32,11 @@ const GetQuestionVote = require('./question/GetQuestionVote');
const Search = require('./question/Search');
router.get('/search', Search);
-
router.post('/', tokenAuth, CreateQuestion);
router.get('/:question_id', GetQuestion);
router.patch('/:question_id', tokenAuth, EditQuestion);
+router.patch('/:question_id/edit', tokenAuth, EditQuestionBody);
+router.patch('/:question_id/addBounty', tokenAuth, EditQuestionBounty);
router.patch('/:question_id/close', tokenAuth, EditQuestionStatusClosed);
router.patch('/:question_id/protect', tokenAuth, EditQuestionStatusProtected);
router.patch('/:question_id/reopen', tokenAuth, EditQuestionStatusReopened);
@@ -56,8 +58,20 @@ router.patch('/:question_id/answers/:answer_id/vote', tokenAuth, EditAnswerVote)
router.get('/:question_id/answers/:answer_id/comments', GetAnswerComments);
router.post('/:question_id/answers/:answer_id/comments', tokenAuth, CreateAnswerComment);
-router.delete('/:question_id/answers/:answer_id/comments/:comment_id', tokenAuth, DeleteAnswerComment);
-router.get('/:question_id/answers/:answer_id/comments/:comment_id/vote', tokenAuth, GetAnswerCommentVote);
-router.patch('/:question_id/answers/:answer_id/comments/:comment_id/vote', tokenAuth, EditAnswerCommentVote);
+router.delete(
+ '/:question_id/answers/:answer_id/comments/:comment_id',
+ tokenAuth,
+ DeleteAnswerComment
+);
+router.get(
+ '/:question_id/answers/:answer_id/comments/:comment_id/vote',
+ tokenAuth,
+ GetAnswerCommentVote
+);
+router.patch(
+ '/:question_id/answers/:answer_id/comments/:comment_id/vote',
+ tokenAuth,
+ EditAnswerCommentVote
+);
module.exports = router;
diff --git a/server/routes/question/CreateAnswer.js b/server/routes/question/CreateAnswer.js
index 6ab37c0..0c44c4b 100644
--- a/server/routes/question/CreateAnswer.js
+++ b/server/routes/question/CreateAnswer.js
@@ -21,7 +21,7 @@ async function CreateAnswer(req, res) {
if (question.status === 'closed') return res.status(403).send(config.errorForbidden);
- if (question.status === 'protected' && getUserLevel(user.points) < 5) {
+ if (question.status === 'protected' && getUserLevel(user.points) < 6) {
return res.status(403).send(config.errorForbidden);
}
diff --git a/server/routes/question/CreateQuestion.js b/server/routes/question/CreateQuestion.js
index 606a4e9..decd845 100644
--- a/server/routes/question/CreateQuestion.js
+++ b/server/routes/question/CreateQuestion.js
@@ -5,10 +5,14 @@ const createRequest = require('server/utils/api');
async function CreateQuestion(req, res) {
const user = req.user;
- const { title, text } = req.body;
+ const { title, text, tags } = req.body;
if (!title || !text) return res.status(400).send(config.errorIncomplete);
+ if (tags && tags.split(' ').length > 5) {
+ return res.status(400).send(config.errorIncomplete);
+ }
+
const { success, question } = await createRequest('post', `/questions`, {
creator: user.username,
title,
@@ -17,7 +21,11 @@ async function CreateQuestion(req, res) {
if (!success) return res.status(500).send(config.errorGeneric);
- const newQuestion = await Question.create({ ...question, _id: question.question_id });
+ const newQuestion = await Question.create({
+ ...question,
+ tags: tags.split(' '),
+ _id: question.question_id,
+ });
// Increment user points by 1
await createRequest('patch', `/users/${user.username}/points`, {
diff --git a/server/routes/question/EditAnswerAccepted.js b/server/routes/question/EditAnswerAccepted.js
index 3d7d8cc..70077a1 100644
--- a/server/routes/question/EditAnswerAccepted.js
+++ b/server/routes/question/EditAnswerAccepted.js
@@ -11,11 +11,13 @@ async function EditAnswerAccepted(req, res) {
// Find question and verify that it exists
const question = await getQuestion(question_id);
- if (!question) return res.status(404).send(config.errorNotFound);
+ if (!question) return res.status(404).send(config.errorNotFound);
+ const { hasBounty } = question;
// Verify user owns question and question does not already have an accepted answer
- if (question.creator !== user.username || question.hasAccepted)
+ if (question.creator !== user.username || question.hasAccepted) {
return res.status(403).send(config.errorForbidden);
+ }
// Patch question with BDPA server
const patchAnswer = await createRequest(
@@ -37,12 +39,22 @@ async function EditAnswerAccepted(req, res) {
await Question.findByIdAndUpdate(question_id, { hasAcceptedAnswer: true });
// Increment points of answer creator
- await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
- operation: 'increment',
- amount: 15,
- });
- await User.findOneAndUpdate({ username: cachedAnswer.creator }, { $inc: { points: 15 } });
-
+ if (hasBounty) {
+ await User.findOneAndUpdate(
+ { username: cachedAnswer.creator },
+ { $inc: { points: 15 + hasBounty } }
+ );
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'increment',
+ amount: 15 + hasBounty,
+ });
+ } else {
+ await User.findOneAndUpdate({ username: cachedAnswer.creator }, { $inc: { points: 15 } });
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'increment',
+ amount: 15,
+ });
+ }
return res.sendStatus(200);
}
diff --git a/server/routes/question/EditAnswerCommentVote.js b/server/routes/question/EditAnswerCommentVote.js
index 49dc66c..21fa1c6 100644
--- a/server/routes/question/EditAnswerCommentVote.js
+++ b/server/routes/question/EditAnswerCommentVote.js
@@ -17,7 +17,7 @@ async function EditAnswerCommentVote(req, res) {
return res.status(403).send(config.errorForbidden);
}
- if (operation === 'downvote' && userLevel < 4) {
+ if (operation === 'downvote' && userLevel < 5) {
return res.status(403).send(config.errorForbidden);
}
diff --git a/server/routes/question/EditAnswerVote.js b/server/routes/question/EditAnswerVote.js
index a0f9d79..11179b5 100644
--- a/server/routes/question/EditAnswerVote.js
+++ b/server/routes/question/EditAnswerVote.js
@@ -18,7 +18,7 @@ async function EditAnswerVote(req, res) {
return res.status(403).send(config.errorForbidden);
}
- if (operation === 'downvote' && userLevel < 4) {
+ if (operation === 'downvote' && userLevel < 5) {
return res.status(403).send(config.errorForbidden);
}
@@ -73,14 +73,10 @@ async function EditAnswerVote(req, res) {
amount: 1,
});
- await createRequest(
- 'patch',
- `/users/${cachedAnswer.creator}/points`,
- {
- operation: 'decrement',
- amount: 15,
- }
- );
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'decrement',
+ amount: 15,
+ });
return res.send({ vote: 'downvoted' });
}
@@ -117,14 +113,10 @@ async function EditAnswerVote(req, res) {
docModel: 'Answer',
});
- await createRequest(
- 'patch',
- `/users/${cachedAnswer.creator}/points`,
- {
- operation: 'increment',
- amount: 15,
- }
- );
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'increment',
+ amount: 15,
+ });
return res.send({ vote: 'upvoted' });
}
@@ -149,14 +141,10 @@ async function EditAnswerVote(req, res) {
docModel: 'Answer',
});
- await createRequest(
- 'patch',
- `/users/${cachedAnswer.creator}/points`,
- {
- operation: 'increment',
- amount: 10,
- }
- );
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'increment',
+ amount: 10,
+ });
return res.send({ vote: 'upvoted' });
} else if (operation === 'downvote') {
@@ -179,14 +167,10 @@ async function EditAnswerVote(req, res) {
amount: 1,
});
- await createRequest(
- 'patch',
- `/users/${cachedAnswer.creator}/points`,
- {
- operation: 'decrement',
- amount: 5,
- }
- );
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'decrement',
+ amount: 5,
+ });
return res.send({ vote: 'downvoted' });
}
diff --git a/server/routes/question/EditCommentVote.js b/server/routes/question/EditCommentVote.js
index ce597e9..cd410de 100644
--- a/server/routes/question/EditCommentVote.js
+++ b/server/routes/question/EditCommentVote.js
@@ -15,7 +15,7 @@ async function EditAnswerVote(req, res) {
return res.status(403).send(config.errorForbidden);
}
- if (operation === 'downvote' && userLevel < 4) {
+ if (operation === 'downvote' && userLevel < 5) {
return res.status(403).send(config.errorForbidden);
}
diff --git a/server/routes/question/EditQuestionBody.js b/server/routes/question/EditQuestionBody.js
new file mode 100644
index 0000000..6410b78
--- /dev/null
+++ b/server/routes/question/EditQuestionBody.js
@@ -0,0 +1,67 @@
+const config = require('server/config.json');
+const Question = require('server/db/models/Question');
+const { getQuestion, refreshQuestion } = require('server/utils/question');
+const createRequest = require('server/utils/api');
+const getUserLevel = require('server/utils/getUserLevel');
+
+async function EditQuestionStatusClosed(req, res) {
+ const { user } = req;
+ const { question_id } = req.params;
+ const { etext, etitle } = req.body;
+ let eObj = [etext, etitle];
+ // Verify user has required level
+ if (getUserLevel(user.points) < 7) {
+ return res.status(403).send(config.errorForbidden);
+ }
+
+ // Get cached question and make sure it exists
+ const question = await getQuestion(question_id);
+ if (!question) return res.status(404).send(config.errorNotFound);
+
+ // Toggle question vote
+ if (question.edit.length === 0) {
+ if (!etext || !etitle) {
+ return res.status(400).send(config.errorIncomplete);
+ }
+
+ await Question.findByIdAndUpdate(question_id, {
+ editText: eObj,
+ $push: { edit: user.username },
+ });
+
+ return res.sendStatus(200);
+ }
+
+ if (question.edit.includes(user.username)) {
+ await Question.findByIdAndUpdate(question_id, {
+ $pull: { edit: user.username },
+ });
+
+ return res.sendStatus(200);
+ } else {
+ await Question.findByIdAndUpdate(question_id, {
+ $push: { close: user.username },
+ });
+ }
+
+ // Patch question status if required
+ if (question.edit.length === 2) {
+ const question = await Question.findByIdAndUpdate(question_id, {
+ edit: [],
+ eObj,
+ });
+
+ const { success } = await createRequest('patch', `/questions/${question_id}`, {
+ etext,
+ eObj,
+ });
+
+ return success
+ ? res.send({ text: question.text })
+ : res.status(500).send(config.errorGeneric);
+ }
+
+ return res.sendStatus(200);
+}
+
+module.exports = EditQuestionStatusClosed;
diff --git a/server/routes/question/EditQuestionBounty.js b/server/routes/question/EditQuestionBounty.js
new file mode 100644
index 0000000..b5d6ba2
--- /dev/null
+++ b/server/routes/question/EditQuestionBounty.js
@@ -0,0 +1,31 @@
+const Question = require('server/db/models/Question');
+const User = require('server/db/models/User');
+const createRequest = require('server/utils/api');
+const getUserLevel = require('server/utils/getUserLevel');
+const config = require('server/config.json');
+
+async function EditQuestionBounty(req, res) {
+ const { user } = req.user;
+ const { question_id } = req.params;
+ const { bounty } = req.body;
+
+ if (!bounty) return res.status(400).send(config.errorIncomplete);
+ const userLevel = getUserLevel(user.points);
+
+ if (userLevel < 4 || user.points < bounty) {
+ return res.status(403).send(config.errorForbidden);
+ }
+
+ await Question.findByIdAndUpdate(question_id, { hasBounty: bounty });
+
+ await User.findOneAndUpdate(
+ { username: cachedAnswer.creator },
+ { $inc: { points: Math.abs(bounty) * -1 } }
+ );
+ await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
+ operation: 'decrement',
+ amount: bounty,
+ });
+}
+
+module.exports = EditQuestionBounty;
diff --git a/server/routes/question/EditQuestionStatusClosed.js b/server/routes/question/EditQuestionStatusClosed.js
index d068d40..52deaff 100644
--- a/server/routes/question/EditQuestionStatusClosed.js
+++ b/server/routes/question/EditQuestionStatusClosed.js
@@ -9,7 +9,7 @@ async function EditQuestionStatusClosed(req, res) {
const { question_id } = req.params;
// Verify user has required level
- if (getUserLevel(user.points) < 7) {
+ if (getUserLevel(user.points) < 9) {
return res.status(403).send(config.errorForbidden);
}
@@ -45,11 +45,9 @@ async function EditQuestionStatusClosed(req, res) {
status: 'closed',
});
- const { success } = await createRequest(
- 'patch',
- `/questions/${question_id}`,
- { status: 'closed' }
- );
+ const { success } = await createRequest('patch', `/questions/${question_id}`, {
+ status: 'closed',
+ });
await Question.findByIdAndUpdate(question_id, { status: 'closed' });
return success
diff --git a/server/routes/question/EditQuestionStatusProtected.js b/server/routes/question/EditQuestionStatusProtected.js
index d3ac29a..f7589b2 100644
--- a/server/routes/question/EditQuestionStatusProtected.js
+++ b/server/routes/question/EditQuestionStatusProtected.js
@@ -9,7 +9,7 @@ async function EditQuestionStatusProtected(req, res) {
const { question_id } = req.params;
// Verify user has required level
- if (getUserLevel(user.points) < 6) {
+ if (getUserLevel(user.points) < 8) {
return res.status(403).send(config.errorForbidden);
}
@@ -18,10 +18,7 @@ async function EditQuestionStatusProtected(req, res) {
if (!question) return res.status(404).send(config.errorNotFound);
// Verify question does not have incompatible status
- if (
- question.status === 'closed' ||
- question.status === 'protected'
- ) {
+ if (question.status === 'closed' || question.status === 'protected') {
return res.status(400).send({
success: false,
error: 'This question is already closed or protected.',
@@ -48,11 +45,11 @@ async function EditQuestionStatusProtected(req, res) {
status: 'protected',
});
- const { success } = await createRequest(
- 'patch',
- `/questions/${question_id}`,
- { status: 'protected' }
- );
+ await User.findOneAndUpdate({ username: creator }, { $push: { badges: 'Protected' } });
+
+ const { success } = await createRequest('patch', `/questions/${question_id}`, {
+ status: 'protected',
+ });
await Question.findByIdAndUpdate(question_id, { status: 'protected' });
return success
diff --git a/server/routes/question/EditQuestionStatusReopened.js b/server/routes/question/EditQuestionStatusReopened.js
index 9c4cb1b..0d54e9d 100644
--- a/server/routes/question/EditQuestionStatusReopened.js
+++ b/server/routes/question/EditQuestionStatusReopened.js
@@ -1,6 +1,6 @@
const config = require('server/config.json');
const Question = require('server/db/models/Question');
-const { getQuestion, refreshQuestion } = require('server/utils/question');
+const { getQuestion } = require('server/utils/question');
const getUserLevel = require('server/utils/getUserLevel');
const createRequest = require('server/utils/api');
@@ -9,7 +9,7 @@ async function EditQuestionStatusReopened(req, res) {
const { question_id } = req.params;
// Verify user has permissions
- if (getUserLevel(user.points) < 7) {
+ if (getUserLevel(user.points) < 9) {
return res.status(403).send(config.errorForbidden);
}
@@ -18,10 +18,7 @@ async function EditQuestionStatusReopened(req, res) {
if (!question) return res.status(404).send(config.errorNotFound);
// Make sure that question has compatible status
- if (
- question.status === 'protected' ||
- question.status === 'open'
- ) {
+ if (question.status === 'protected' || question.status === 'open') {
return res.status(400).send({
success: false,
error: 'This question is already open.',
@@ -48,11 +45,11 @@ async function EditQuestionStatusReopened(req, res) {
status: 'open',
});
- const { success } = await createRequest(
- 'patch',
- `/questions/${question_id}`,
- { status: 'open' }
- );
+ await User.findOneAndUpdate({ username: creator }, { $push: { badges: 'Zombie' } });
+
+ const { success } = await createRequest('patch', `/questions/${question_id}`, {
+ status: 'open',
+ });
await Question.findByIdAndUpdate(question_id, { status: 'open' });
return success
diff --git a/server/routes/question/EditQuestionVote.js b/server/routes/question/EditQuestionVote.js
index a50e5fe..2410484 100644
--- a/server/routes/question/EditQuestionVote.js
+++ b/server/routes/question/EditQuestionVote.js
@@ -21,7 +21,7 @@ async function EditQuestionVote(req, res) {
return res.status(403).send(config.errorForbidden);
}
- if (operation === 'downvote' && userLevel < 4) {
+ if (operation === 'downvote' && userLevel < 5) {
return res.status(403).send(config.errorForbidden);
}
@@ -75,10 +75,7 @@ async function EditQuestionVote(req, res) {
operation: 'decrement',
amount: 6,
});
- await User.findOneAndUpdate(
- { username: question.creator },
- { $inc: { points: -6 } }
- );
+ await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: -6 } });
// Decrement the user's points
await createRequest('patch', `/users/${user.username}/points`, {
@@ -90,19 +87,12 @@ async function EditQuestionVote(req, res) {
return res.send({ vote: 'downvoted' });
} else {
// Decrement the question creator's points
- const { success } = await createRequest(
- 'patch',
- `/users/${question.creator}/points`,
- {
- operation: 'decrement',
- amount: 5,
- }
- );
+ const { success } = await createRequest('patch', `/users/${question.creator}/points`, {
+ operation: 'decrement',
+ amount: 5,
+ });
- await User.findOneAndUpdate(
- { username: question.creator },
- { $inc: { points: -5 } }
- );
+ await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: -5 } });
if (!success) return res.status(500).send(config.errorGeneric);
@@ -139,10 +129,7 @@ async function EditQuestionVote(req, res) {
operation: 'increment',
amount: 6,
});
- await User.findOneAndUpdate(
- { username: question.creator },
- { $inc: { points: 6 } }
- );
+ await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: 6 } });
return res.send({ vote: 'upvoted' });
} else {
@@ -151,10 +138,7 @@ async function EditQuestionVote(req, res) {
operation: 'increment',
amount: 1,
});
- await User.findOneAndUpdate(
- { username: question.creator },
- { $inc: { points: 1 } }
- );
+ await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: 1 } });
// Increment the user's points
await createRequest('patch', `/users/${user.username}/points`, {
@@ -188,10 +172,7 @@ async function EditQuestionVote(req, res) {
operation: 'increment',
amount: 5,
});
- await User.findOneAndUpdate(
- { username: question.creator },
- { $inc: { points: 5 } }
- );
+ await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: 5 } });
return res.send({ vote: 'upvoted' });
} else if (operation === 'downvote') {
@@ -218,10 +199,7 @@ async function EditQuestionVote(req, res) {
operation: 'decrement',
amount: 1,
});
- await User.findOneAndUpdate(
- { username: question.creator },
- { $inc: { points: -1 } }
- );
+ await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: -1 } });
// Decrement the user's points
await createRequest('patch', `/users/${user.username}/points`, {
diff --git a/server/routes/question/Search.js b/server/routes/question/Search.js
index eb23464..eb5311a 100644
--- a/server/routes/question/Search.js
+++ b/server/routes/question/Search.js
@@ -1,32 +1,59 @@
-const createRequest = require('server/utils/api');
const config = require('server/config.json');
const Question = require('server/db/models/Question');
async function Search(req, res) {
- const { success, questions } = await createRequest(
- 'get',
- `/questions/search`,
- req.query
- );
+ const { creator, title, text, tags, createdAt, sort } = req.query;
- if (!success) return res.status(500).send(config.errorGeneric);
+ let searchQuery = {};
+ if (creator) {
+ searchQuery['creator'] = {
+ $regex: creator,
+ };
+ }
+ if (title) {
+ searchQuery['title'] = {
+ $regex: title,
+ };
+ }
+ if (text) {
+ searchQuery['text'] = {
+ $regex: text,
+ };
+ }
+ if (tags) {
+ searchQuery['tags'] = { $all: tags };
+ }
+ if (createdAt) {
+ searchQuery['createdAt'] = createdAt;
+ }
- // Patch search results to database
- const questionSet = await Promise.all(
- questions
- .map((question) => ({
- id: question.question_id,
- ...question,
- }))
- .map(async (question) => {
- return Question.findByIdAndUpdate(question.id, question, {
- upsert: true,
- });
- })
- );
+ let sortQuery = [[createdAt, 'desc']];
+ switch (sort) {
+ case 'u':
+ sortQuery = [['upvotes', 'desc']];
+ break;
+ case 'uvc':
+ sortQuery = [
+ ['upvotes', 'desc'],
+ ['view', 'desc'],
+ ['comments', 'desc'],
+ ];
+ break;
+ case 'uvac':
+ sortQuery = [
+ ['upvotes', 'desc'],
+ ['view', 'desc'],
+ ['answers', 'desc'],
+ ['comments', 'desc'],
+ ];
+ searchQuery['hasAcceptedAnswer'] = false;
+ break;
+ }
- return res.send({ questions: questionSet.filter(question => question) });
+ const questions = await Question.find(searchQuery).sort(sortQuery);
+
+ return res.send({ questions });
}
module.exports = Search;
diff --git a/server/routes/user.js b/server/routes/user.js
index e786963..eb42caa 100644
--- a/server/routes/user.js
+++ b/server/routes/user.js
@@ -5,6 +5,7 @@ const basicAuth = require('server/middleware/basicAuth');
const tokenAuth = require('server/middleware/tokenAuth');
const Answers = require('./user/Answers');
+const GetBadges = require('./user/GetBadges');
const Delete = require('./user/Delete');
const Edit = require('./user/Edit');
const GetUser = require('./user/GetUser');
@@ -20,6 +21,7 @@ router.post('/', Register);
router.patch('/', tokenAuth, Edit);
router.get('/questions', tokenAuth, Questions);
router.get('/answers', tokenAuth, Answers);
+router.get('/badges', tokenAuth, GetBadges);
router.post('/login', basicAuth, Login);
router.get('/remember', tokenAuth, Remember);
router.delete('/logout', tokenAuth, Logout);
diff --git a/server/routes/user/GetBadges.js b/server/routes/user/GetBadges.js
new file mode 100644
index 0000000..fc83dd4
--- /dev/null
+++ b/server/routes/user/GetBadges.js
@@ -0,0 +1,24 @@
+const Badge = require('server/db/models/Badge');
+
+async function GetBadges(req, res) {
+ const { user } = req;
+
+ const badges = await Badge.find();
+
+ const response = badges.reduce(
+ (acc, badge) => {
+ if (user.badges.includes(badge.title)) {
+ acc.obtained.push(badge);
+ } else {
+ acc.unobtained.push(badge);
+ }
+
+ return acc;
+ },
+ { obtained: [], unobtained: [] }
+ );
+
+ return res.send({ badges: response });
+}
+
+module.exports = GetBadges;
diff --git a/server/routes/user/GetUser.js b/server/routes/user/GetUser.js
index 25ddd93..9e0b720 100644
--- a/server/routes/user/GetUser.js
+++ b/server/routes/user/GetUser.js
@@ -2,6 +2,7 @@ const createRequest = require('server/utils/api');
const config = require('server/config.json');
const User = require('server/db/models/User');
const getUserLevel = require('server/utils/getUserLevel');
+const Badge = require('server/db/models/Badge');
async function GetUser(req, res) {
const { username } = req.params;
@@ -27,12 +28,34 @@ async function GetUser(req, res) {
new: true,
});
+ const badges = await Badge.find({ title: { $in: user.badges } });
+
+ const badgeCount = badges.reduce(
+ (acc, badge) => {
+ if (badge.rank === 'Gold') {
+ acc.gold = acc.gold++;
+ } else if (badge.rank === 'Silver') {
+ acc.silver = acc.silver++;
+ } else if (badge.rank === 'Bronze') {
+ acc.bronze = acc.bronze++;
+ }
+
+ return acc;
+ },
+ {
+ gold: 0,
+ silver: 0,
+ bronze: 0,
+ }
+ );
+
return res.send({
user: {
username: newUser.username,
email: newUser.email,
points: newUser.points,
level: getUserLevel(newUser.points),
+ badgeCount,
},
});
}
diff --git a/server/routes/user/Login.js b/server/routes/user/Login.js
index 3c2d933..8b40d9f 100644
--- a/server/routes/user/Login.js
+++ b/server/routes/user/Login.js
@@ -2,6 +2,7 @@ const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const Token = require('server/db/models/Token');
+const Badge = require('server/db/models/Badge');
const getUserLevel = require('server/utils/getUserLevel');
async function Login(req, res) {
@@ -20,6 +21,27 @@ async function Login(req, res) {
user: user.username,
});
+ const badges = await Badge.find({ title: { $in: user.badges } });
+
+ const badgeCount = badges.reduce(
+ (acc, badge) => {
+ if (badge.rank === 'Gold') {
+ acc.gold = acc.gold + 1;
+ } else if (badge.rank === 'Silver') {
+ acc.silver = acc.silver + 1;
+ } else if (badge.rank === 'Bronze') {
+ acc.bronze = acc.bronze + 1;
+ }
+
+ return acc;
+ },
+ {
+ gold: 0,
+ silver: 0,
+ bronze: 0,
+ }
+ );
+
return res.send({
token: token.token,
user: {
@@ -27,7 +49,8 @@ async function Login(req, res) {
email: user.email,
points: user.points,
level: getUserLevel(user.points),
- }
+ badgeCount,
+ },
});
}
diff --git a/server/routes/user/Remember.js b/server/routes/user/Remember.js
index 3c6ea79..8507fed 100644
--- a/server/routes/user/Remember.js
+++ b/server/routes/user/Remember.js
@@ -1,15 +1,38 @@
const getUserLevel = require('server/utils/getUserLevel');
+const Badge = require('../../db/models/Badge');
async function Remember(req, res) {
const { user } = req;
+ const badges = await Badge.find({ title: { $in: user.badges } });
+
+ const badgeCount = badges.reduce(
+ (acc, badge) => {
+ if (badge.rank === 'Gold') {
+ acc.gold = acc.gold + 1;
+ } else if (badge.rank === 'Silver') {
+ acc.silver = acc.silver + 1;
+ } else if (badge.rank === 'Bronze') {
+ acc.bronze = acc.bronze + 1;
+ }
+
+ return acc;
+ },
+ {
+ gold: 0,
+ silver: 0,
+ bronze: 0,
+ }
+ );
+
return res.send({
user: {
username: user.username,
email: user.email,
points: user.points,
level: getUserLevel(user.points),
- }
+ badgeCount,
+ },
});
}
diff --git a/server/utils/badges/answerBadges.js b/server/utils/badges/answerBadges.js
new file mode 100644
index 0000000..4a47e1c
--- /dev/null
+++ b/server/utils/badges/answerBadges.js
@@ -0,0 +1,17 @@
+function calculateAnswerBadges(points) {
+ let badges = [];
+
+ if (points >= 100) {
+ badges.push('Great Answer');
+ }
+ if (points >= 25) {
+ badges.push('Good Answer');
+ }
+ if (points >= 10) {
+ badges.push('Nice Answer');
+ }
+
+ return badges;
+}
+
+module.exports = calculateAnswerBadges;
diff --git a/server/utils/badges/questionBadges.js b/server/utils/badges/questionBadges.js
new file mode 100644
index 0000000..cdf9437
--- /dev/null
+++ b/server/utils/badges/questionBadges.js
@@ -0,0 +1,17 @@
+function calculateQuestionBadges(points) {
+ let badges = [];
+
+ if (points >= 100) {
+ badges.push('Great Question');
+ }
+ if (points >= 25) {
+ badges.push('Good Question');
+ }
+ if (points >= 10) {
+ badges.push('Nice Question');
+ }
+
+ return badges;
+}
+
+module.exports = calculateQuestionBadges;
diff --git a/server/utils/badges/userBadges.js b/server/utils/badges/userBadges.js
new file mode 100644
index 0000000..c8bc1f0
--- /dev/null
+++ b/server/utils/badges/userBadges.js
@@ -0,0 +1,17 @@
+function calculateUserBadges(points) {
+ let badges = [];
+
+ if (points >= 10000) {
+ badges.push('Socratic');
+ }
+ if (points >= 3000) {
+ badges.push('Inquisitive');
+ }
+ if (points >= 100) {
+ badges.push('Curious');
+ }
+
+ return badges;
+}
+
+module.exports = calculateUserBadges;
diff --git a/server/utils/getUserLevel.js b/server/utils/getUserLevel.js
index dbbb9b2..08fce3d 100644
--- a/server/utils/getUserLevel.js
+++ b/server/utils/getUserLevel.js
@@ -1,20 +1,14 @@
function getUserLevel(points) {
- if (points > 10000)
- return 7;
- else if (points > 3000)
- return 6;
- else if (points > 1000)
- return 5;
- else if (points > 125)
- return 4;
- else if (points > 50)
- return 3;
- else if (points > 15)
- return 2;
- else if (points > 0)
- return 1;
- else
- return 0;
+ if (points > 10000) return 9;
+ else if (points > 3000) return 8;
+ else if (points > 2000) return 7;
+ else if (points > 1000) return 6;
+ else if (points > 125) return 5;
+ else if (points > 75) return 4;
+ else if (points > 50) return 3;
+ else if (points > 15) return 2;
+ else if (points > 0) return 1;
+ else return 0;
}
module.exports = getUserLevel;