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({ + + + {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(', ')} +
+ {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({ + + + 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 ( +
+ + + + + + {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() {