Merge branch 'main' of github.com:spicecat/qOverflow

This commit is contained in:
Ceferino Patino 2022-08-02 17:57:23 -05:00
commit 046c0d61fe
23 changed files with 215 additions and 141 deletions

View file

@ -1,12 +1,12 @@
import { ButtonGroup, ListItem, ListItemText, Tooltip } from '@mui/material';
import { Button, ButtonGroup, ListItem, ListItemText, Tooltip } from '@mui/material';
import CheckIcon from '@mui/icons-material/Check';
import ReactMarkdown from 'react-markdown';
import CreationInfoTag from 'components/CreationInfoTag';
import { useUser } from 'contexts';
import { AnswerCommentsList, CommentControl, VoteControl } from 'controllers/QAControllers';
import { CreationInfoTag } from 'controllers';
import { getAnswerVote, updateAnswerVote, postAnswerComment } from 'services/questionsServices';
import { getAnswerVote, updateAcceptAnswer, updateAnswerVote, updateQuestion } from 'services/questionsServices';
import { useQuestion } from 'contexts';
import { postQuestionComment, postAnswerComment } from 'services/questionsServices';
export default function Answer({
accepted,
answer_id,
@ -23,21 +23,22 @@ export default function Answer({
const getVote = () => getAnswerVote(question_id, answer_id);
const updateVote = (data) => updateAnswerVote(question_id, answer_id, data);
const {permissions} = useQuestion();
let canVote = permissions.canVote;
let canComment = permissions.canComment;
let canAccept = permissions.canAccept;
function acceptAnswer(){
updateAcceptAnswer(question_id, answer_id)
}
const postComment = (data) => postAnswerComment(question_id, answer_id, data);
return (
<span key={answer_id}>
<ListItem disablePadding>
<ButtonGroup orientation='vertical'>
<VoteControl
{...{
downvotes,
getVote,
orientation: 'vertical',
updateVote,
upvotes,
}}
/>
<VoteControl {...{ downvotes, getVote, orientation: 'vertical', updateVote, upvotes, canVote }} />
{accepted && (
<div style={{ textAlign: 'center' }}>
<Tooltip title='Accepted Answer' placement='right'>
@ -47,14 +48,12 @@ export default function Answer({
)}
</ButtonGroup>
<ListItemText>
<CreationInfoTag
{...{ createdAt, creator, text: 'answered' }}
/>
<ReactMarkdown>{text}</ReactMarkdown>
<CommentControl {...{
canComment: level >= 3 || creator === username,
postComment
}} />
<CreationInfoTag {...{ createdAt, creator, text: 'answered' }} />
<ReactMarkdown>
{text}
</ReactMarkdown>
<CommentControl {...{canComment, postComment}} />
<Tooltip title = {!canAccept && "Only the creator can accept, or an answer is already accepted"}><span><Button onClick = {acceptAnswer} disabled = {!canAccept} style = {{'marginLeft':'10px'}}variant = "standard">Accept</Button></span></Tooltip>
</ListItemText>
</ListItem>
<ListItem sx={{ pl: 8 }}>

View file

@ -2,7 +2,7 @@ import { Divider, ListItem, ListItemText } from '@mui/material';
import { CreationInfoTag } from 'controllers';
import { VoteControl } from 'controllers/QAControllers';
import { getAnswerCommentVote, updateAnswerCommentVote } from 'services/questionsServices';
import { useQuestion } from 'contexts';
export default function AnswerComment({
answer_id,
comment_id,
@ -15,7 +15,8 @@ export default function AnswerComment({
}) {
const getVote = () => getAnswerCommentVote(question_id, answer_id, comment_id);
const updateVote = (data) => updateAnswerCommentVote(question_id, answer_id, comment_id, data);
const {permissions} = useQuestion();
let canVote = permissions.canVote;
return (
<span key={comment_id}>
<ListItem disablePadding>
@ -23,7 +24,7 @@ export default function AnswerComment({
<CreationInfoTag {...{ createdAt, creator, text: 'commented' }} />
{text}
</ListItemText>
<VoteControl {...{ downvotes, getVote, updateVote, upvotes }} />
<VoteControl {...{ downvotes, getVote, updateVote, upvotes, canVote }} />
</ListItem >
<Divider />
</span >

View file

@ -2,6 +2,7 @@ import { Divider, ListItem, ListItemText } from '@mui/material';
import { CreationInfoTag } from 'controllers';
import { VoteControl } from 'controllers/QAControllers';
import { getCommentVote, updateCommentVote } from 'services/questionsServices';
import { useQuestion } from 'contexts';
export default function Comment({
comment_id,
@ -10,11 +11,13 @@ export default function Comment({
downvotes,
question_id,
text,
upvotes
upvotes,
}) {
const getVote = () => getCommentVote(question_id, comment_id);
const updateVote = (data) => updateCommentVote(question_id, comment_id, data);
const {permissions} = useQuestion();
let canVote = permissions.canVote;
return (
<span key={comment_id}>
<ListItem disablePadding>
@ -22,7 +25,7 @@ export default function Comment({
<CreationInfoTag {...{ createdAt, creator, text: 'commented' }} />
{text}
</ListItemText>
<VoteControl {...{ downvotes, getVote, updateVote, upvotes }} />
<VoteControl {...{ downvotes, getVote, updateVote, upvotes, canVote}} />
</ListItem>
<Divider />
</span>

View file

@ -8,9 +8,11 @@ export default function CommentControl({
commentSchema,
postComment,
show,
toggleShow
toggleShow,
}) {
return (
<span>
<ButtonGroup style={{ alignItems: 'center' }}>

View file

@ -11,10 +11,10 @@ export default function CreateAnswer({
return (
<div>
<Tooltip title={canAnswer ? '' : 'You need to be authenticated to answer or level 5 if the question is protected'}>
<Tooltip title={!canAnswer ? '' : 'You need to be authenticated to answer or level 5 if the question is protected'}>
<span>
<Button
disabled={!canAnswer}
disabled={canAnswer}
variant='contained'
onClick={toggleShow}
>

View file

@ -9,18 +9,13 @@ import {
Tooltip,
} from '@mui/material';
import ReactMarkdown from 'react-markdown';
import { Link } from 'react-router-dom';
import { CreationInfoTag } from 'controllers';
import { CommentControl, CreateAnswer, VoteControl } from 'controllers/QAControllers';
import { useUser } from 'contexts';
import {
getQuestionVote,
postQuestionComment,
editQuestionStatus,
updateQuestionVote,
} from 'services/questionsServices';
import { CommentControl, VoteControl } from 'controllers/QAControllers';
import { getQuestionVote, postQuestionComment,openQuestion,protectQuestion , updateQuestion, closeQuestion, updateQuestionVote } from 'services/questionsServices';
import { Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { useQuestion, useUser } from 'contexts';
import CreateAnswer from './CreateAnswer';
const statusColor = (status) => {
switch (status) {
case 'open':
@ -47,19 +42,67 @@ export default function Question({
question_id,
upvotes,
views,
protect,
close,
reopen
}) {
const { userData: { level, username } } = useUser();
const protect = level >= 6 && status === 'open';
const close = level >= 7;
const {userData} = useUser();
const [ongoingVote, setOngoingVote] = useState({})
const {setPermissions} = useQuestion();
function setVote(){
if(protect.length > 0){setOngoingVote({users : protect, type : "protect"})}
if(close.length > 0){setOngoingVote({users : close, type : "close"})}
if(reopen.length > 0){setOngoingVote({users : reopen, type : "open"})}
}
function changeProtect() {
editQuestionStatus(question_id, 'protect');
let level = 0;
let protection = false;
let canProtect = false;
let canClose = false;
let canComment = false;
let canAnswer = true;
let canVote = true;
let canAccept = false;
if(userData.username){
level = userData.level;
if(status === 'protected' || status === 'closed'){protection = true;}
if(userData.username === creator && !hasAcceptedAnswer){canAccept = true}
if(level >= 7 && ongoingVote.type !== "protect"){canClose = true}
if(level >= 6 && !protection && ongoingVote.type !== "close"){canProtect = true}
if((level >= 3 && !protection) || (status === 'protected' && level >= 5) || (userData.username === creator)){canComment = true}
if((status === 'protected' && level < 5) || status === 'closed'){canAnswer = false}
if(status === 'closed' || level < 2){canVote = false}
}else{
canAnswer = false;
}
useEffect(()=>{
setPermissions({canVote: canVote, canComment: canComment, canAccept: canAccept })
setVote();
},[canVote, canComment, canAccept])
function changeClose() {
const operation = status === 'open' ? 'close' : 'reopen';
editQuestionStatus(question_id, operation);
useEffect(()=>{
updateQuestion(question_id, {views: "increment"})
},[])
function changeProtect(){
protectQuestion(question_id, userData)
}
function changeClose(){
if(status === 'open'){
closeQuestion(question_id, userData)
}else{
openQuestion(question_id, userData)
}
}
const getVote = () => getQuestionVote(question_id);
@ -87,44 +130,21 @@ export default function Question({
label={hasAcceptedAnswer ? 'yes' : 'no'}
size='small'
/>
<Button
component={Link}
display='inline'
m={1}
style={{ marginLeft: '10px' }}
to='../ask'
variant='contained'
>
Ask question
</Button>
<Tooltip title={close ? '' : 'You must be level 7'}>
<Button component={Link} to='../ask' style={{'marginLeft': '10px'}}display = 'inline' m = {1} variant = "contained">Ask question</Button>
<Tooltip title = {!canClose && "You must be level 7"}>
<span>
<Button
disabled={!close}
style={{ marginLeft: '10px' }}
display='inline'
m={1}
onClick={changeProtect}
variant='contained'
>
Close/Open
</Button>
<Button disabled = {!canClose} style={{'marginLeft': '10px'}}display = 'inline' m = {1} variant = "contained" onClick = {changeClose}>Close/Open</Button>
</span>
</Tooltip>
<Tooltip title={protect ? '' : 'You must be level 6 and this question must be open'}>
<Tooltip title = {!canProtect && "You must be level 6 and this question must be open" }>
<span>
<Button
disabled={!protect}
style={{ marginLeft: '10px' }}
display='inline'
m={1}
variant='contained'
onClick={changeClose}
>
Protect
</Button>
<Button disabled = {!canProtect} style={{'marginLeft': '10px'}}display = 'inline' m = {1} variant = "contained" onClick = {changeProtect}>Protect</Button>
</span>
</Tooltip>
{Object.keys(ongoingVote).length > 0 && <Typography>{ongoingVote.users.toString()} - voting to {ongoingVote.type} this question </Typography>}
</Box>
<Divider />
@ -140,14 +160,10 @@ export default function Question({
/>
<ListItemText>
<CreationInfoTag {...{ createdAt, creator }} />
<ReactMarkdown>{text}</ReactMarkdown>
<CommentControl {...{
canComment:
(level >= 3 && status === 'open')
|| (level >= 5 && status === 'protected')
|| creator === username,
postComment
}} />
<ReactMarkdown>
{text}
</ReactMarkdown>
<CommentControl {...{ postComment, canComment}} />
</ListItemText>
</ListItem>
<CreateAnswer />

View file

@ -12,7 +12,7 @@ export default function VoteControl({
upvotes,
vote
}) {
return (
<span style={{ marginRight: 8 }}>
<ButtonGroup {...{ orientation, style: { alignItems: 'center' } }}>

View file

@ -24,11 +24,11 @@ export default function Buffet() {
const [sort, setSort] = useState(0);
const getData = async ({ question_id }) => {
const getData = async () => {
const { error, questions } = await searchQuestions({
...sortObjArr[sort],
after: question_id
...sortObjArr[sort]
});
if (error) {
setError(error);

View file

@ -29,15 +29,13 @@ export default function Dashboard() {
setCurrent(value);
};
const getAnswers = async ({ answer_id }) => {
const { error, answers } = await getUserAnswers({ after: answer_id });
const getAnswers = async () => {
const { error, answers } = await getUserAnswers();
if (!error) return answers;
};
const getQuestions = async ({ question_id }) => {
const { error, questions } = await getUserQuestions({
after: question_id,
});
const getQuestions = async () => {
const { error, questions } = await getUserQuestions();
if (!error) return questions;
};

View file

@ -9,7 +9,7 @@ import { searchQuestions } from 'services/questionsServices';
export default function Search() {
const [searchParams] = useSearchParams();
const getData = async ({ question_id }) => {
const getData = async () => {
let match = {};
const regexMatch = {}
const time = {};
@ -32,7 +32,7 @@ export default function Search() {
const title = searchParams.get('title');
if (title) regexMatch.title = title.replaceAll(' ', '|');
const { questions } = await searchQuestions({ after: question_id, regexMatch, match })
const { questions } = await searchQuestions({ regexMatch, match })
return questions;
}
@ -45,7 +45,7 @@ export default function Search() {
<SearchForm />
</CardContent>
</Card>
<PaginatedList {...{ Component: ListQuestion, getData }} />;
<PaginatedList {...{ concat: true, Component: ListQuestion, getData }} />;
</div>
)
}

View file

@ -10,6 +10,8 @@ const QuestionContext = createContext();
export default function QuestionProvider({ children }) {
const { question_id } = useParams();
const [questionData, setQuestionData] = useState(initialQuestionData);
const [permissions, setPermissions] = useState({})
useEffect(() => {
const loadQuestion = async () => {
@ -21,7 +23,7 @@ export default function QuestionProvider({ children }) {
}, [question_id]);
return (
<QuestionContext.Provider value={{ questionData }}>
<QuestionContext.Provider value={{ questionData, permissions, setPermissions }}>
<Helmet>
<title>{questionData.title}</title>
</Helmet>

View file

@ -11,8 +11,9 @@ export default function AskFormController() {
const askQuestion = async (fields) => {
const { status } = await postQuestion(fields);
if (status === 200)
navigate('/');
if (status === "success") {
navigate('../');
}
}
return (

View file

@ -3,17 +3,19 @@ import { PaginatedList } from 'components';
import { useSearchParams } from 'react-router-dom';
const rowsPerPage = 5;
export default function PaginatedListController({
concat = true,
concat = false,
count,
Component,
getData,
noData,
}) {
const [searchParams] = useSearchParams();
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [load, setLoad] = useState(true);
const searchParams = useSearchParams();
const [load, setLoad] = useState(concat);
const handleChangePage = (_, newPage) => {
setPage(newPage);
@ -21,13 +23,12 @@ export default function PaginatedListController({
loadData();
};
const loadData = async () => {
const newData = await getData(
concat ? data[data.length - 1] ?? {} : {}
);
if (newData.length)
if (concat) setData(data.concat(newData));
else setData(newData);
const loadData = async (clear) => {
const newData = await getData(clear ? {} : data[data.length - 1] ?? {});
if (newData?.length)
if (clear) setData(newData);
else setData(data.concat(newData));
else setLoad(false);
};

View file

@ -4,19 +4,21 @@ import { CommentControl } from 'components/QAComponents';
import { commentFields } from 'services/fields';
import { commentSchema } from 'services/schemas';
export default function CommentControlController({ canComment, postComment }) {
const [show, setShow] = useState(false);
export default function CommentControlController({ postComment, canComment }) {
const [show, setShow] = useState(false)
const toggleShow = () => {
setShow(!show);
};
return CommentControl({
canComment,
commentFields,
commentSchema,
postComment,
show,
toggleShow,
});
return (
CommentControl({
commentFields,
commentSchema,
postComment,
show,
toggleShow,
canComment
})
);
}

View file

@ -6,8 +6,8 @@ import { getAnswerComments, getAnswers, getQuestionComments } from 'services/que
export function AnswerCommentsList({ answer_id, comments: count }) {
const { questionData: { question_id } } = useQuestion();
const getData = () =>
getAnswerComments(question_id, answer_id)
const getData = ({ comment_id }) =>
getAnswerComments(question_id, answer_id, { after: comment_id })
.then(({ comments }) => comments.map(comment => ({ ...comment, answer_id, question_id })))
.catch(() => []);

View file

@ -8,6 +8,7 @@ export default function VoteControlController({
orientation,
updateVote,
upvotes,
canVote
}) {
const { questionData: { status } } = useQuestion();
const { userData: { level } } = useUser();
@ -63,7 +64,8 @@ export default function VoteControlController({
handleUpvote,
orientation,
upvotes: upvotes + (vote === 'upvoted') - (original === 'upvoted'),
vote
vote,
canVote
})
);
}

View file

@ -11,7 +11,30 @@ const getQuestion = async (question_id) =>
callQuestionsAPI('get', `/${question_id}`);
const updateQuestion = async (question_id, data) =>
callQuestionsAPI('patch', `/${question_id}`, data);
callQuestionsAPI(
'patch',
`/${question_id}`,
data
);
const closeQuestion = async (question_id, data) =>
callQuestionsAPI(
'patch',
`/${question_id}/close`,
data
);
const openQuestion = async (question_id, data) =>
callQuestionsAPI(
'patch',
`/${question_id}/reopen`,
data
);
const protectQuestion = async (question_id, data) =>
callQuestionsAPI(
'patch',
`/${question_id}/protect`,
data
);
const getQuestionVote = async (question_id) =>
callQuestionsAPI('get', `/${question_id}/vote`);
@ -48,7 +71,10 @@ const updateAnswer = async (question_id, answer_id, data) =>
callQuestionsAPI('patch', `/${question_id}/answers/${answer_id}`, data);
const updateAcceptAnswer = async (question_id, answer_id) =>
callQuestionsAPI('patch', `/${question_id}/answer/${answer_id}/accept`);
callQuestionsAPI(
'patch',
`/${question_id}/answers/${answer_id}/accept`
);
const getAnswerVote = async (question_id, answer_id) =>
callQuestionsAPI('get', `/${question_id}/answers/${answer_id}/vote`);
@ -120,5 +146,7 @@ export {
updateCommentVote,
updateQuestion,
updateQuestionVote,
editQuestionStatus,
closeQuestion,
openQuestion,
protectQuestion
};

View file

@ -49,7 +49,7 @@ async function CreateAnswer(req, res) {
amount: 2,
});
await User.findByIdAndUpdate(user.id, { $inc: { $points: 2 } });
await User.findByIdAndUpdate(user.id, { $inc: { points: 2 } });
return res.sendStatus(200);
}

View file

@ -30,6 +30,10 @@ async function EditAnswerAccepted(req, res) {
}
const cachedAnswer = await Answer.findByIdAndDelete(answer_id);
//update hasAcceptedAnswer
await createRequest('patch', `/questions/${question_id}`, {
hasAcceptedAnswer: true
});
// Increment points of answer creator
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {

View file

@ -6,8 +6,23 @@ const createRequest = require('server/utils/api');
async function EditQuestion(req, res) {
const { user } = req;
const { text } = req.body;
const {views} = req.body;
const { question_id } = req.params;
if(views){
const { success } = await createRequest(
'patch',
`/questions/${question_id}`,
{ views }
);
if (!success) return res.status(500).send(config.errorGeneric);
await refreshQuestion(question_id);
return res.sendStatus(200);
}
if (!text) return res.status(400).send(config.errorIncomplete);
// Verify user owns question

View file

@ -28,13 +28,13 @@ async function EditQuestionStatusClosed(req, res) {
// Toggle question vote
if (question.close.includes(user.username)) {
await Question.findByIdAndUpdate(question_id, {
close: { $pull: user.username },
$pull: { close: user.username },
});
return res.sendStatus(200);
} else {
await Question.findByIdAndUpdate(question_id, {
close: { $push: user.username },
$push: { close: user.username },
});
}

View file

@ -31,13 +31,13 @@ async function EditQuestionStatusProtected(req, res) {
// Toggle vote
if (question.protect.includes(user.username)) {
await Question.findByIdAndUpdate(question_id, {
protect: { $pull: user.username },
$pull: { protect: user.username },
});
return res.sendStatus(200);
} else {
await Question.findByIdAndUpdate(question_id, {
protect: { $push: user.username },
$push: { protect: user.username },
});
}

View file

@ -31,13 +31,13 @@ async function EditQuestionStatusReopened(req, res) {
// Toggle vote
if (question.reopen.includes(user.username)) {
await Question.findByIdAndUpdate(question_id, {
reopen: { $pull: user.username },
$pull: { reopen: user.username },
});
return res.sendStatus(200);
} else {
await Question.findByIdAndUpdate(question_id, {
reopen: { $push: user.username },
$push: { reopen: user.username },
});
}