Compare commits

...

109 commits

Author SHA1 Message Date
643d0c7933 Updated build scripts 2022-12-19 17:59:28 -06:00
604e7f22f6 Added docker to project 2022-12-11 22:07:56 -06:00
2cc28dd77b Removed concat from Buffet 2022-08-23 20:18:29 -05:00
5308c428b9 Removed eslint 2022-08-23 20:16:55 -05:00
3d1e9110f6 Added eslint 2022-08-23 09:44:45 -04:00
68e0a9e33a Changed server imports to relative imports 2022-08-23 09:31:58 -04:00
fd8bd222eb Updated package.json 2022-08-23 09:13:37 -04:00
c284809fd8 Added build folder from public 2022-08-18 18:47:17 -04:00
300b9e296b Removed console.logs and uncommented cron 2022-08-18 18:35:59 -04:00
2fb612f139
Merge pull request #32 from spicecat/dev
Dev
2022-08-18 18:30:06 -04:00
2e3e92a8da Fixed badgeCount 2022-08-18 18:28:04 -04:00
5769bc9c81 Edited listquestion 2022-08-18 18:26:13 -04:00
“spicecat”
a6f4708f64 indent mobile navbar 2022-08-18 18:24:03 -04:00
“spicecat”
f6249cdb8c lint 2022-08-18 18:19:43 -04:00
“spicecat”
4efa8b2f65 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 18:07:57 -04:00
“spicecat”
6d3a77cd83 improve button margins 2022-08-18 18:07:54 -04:00
tejusk2
bc3e0ca577 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:03:02 -05:00
tejusk2
c59f801996 QA close/open permissions fix 2022-08-18 17:02:56 -05:00
“spicecat”
e1276bb288 remove read mail text 2022-08-18 18:00:52 -04:00
“spicecat”
18bbccdd17 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:58:29 -04:00
“spicecat”
3bb7711158 weird share answer duplicate fix 2022-08-18 17:58:26 -04:00
255575bc9b
Merge pull request #31 from spicecat/levels
Levels
2022-08-18 17:54:29 -04:00
7f8111bacd Fixed question bounty 2022-08-18 17:53:58 -04:00
“spicecat”
2791c59ec3 fix dashboard badges 2022-08-18 17:48:19 -04:00
“spicecat”
f46f407d6b Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:47:22 -04:00
“spicecat”
dedf809cd6 test 2022-08-18 17:47:19 -04:00
tejusk2
f8209c89cd Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 16:45:19 -05:00
tejusk2
c253c3bd9d change 2022-08-18 16:45:13 -05:00
Ani Simhadri
a5a073c50e Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:40:03 -04:00
Ani Simhadri
90b5027931 fix badges 2022-08-18 17:36:57 -04:00
d07f866ef6 Fixed user permissions 2022-08-18 17:35:12 -04:00
tejusk2
aa854f62e7 fixed sorting 2022-08-18 16:35:00 -05:00
tejusk2
be699c329b Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 16:27:57 -05:00
tejusk2
bb6452b15d level tooltips changed 2022-08-18 16:27:50 -05:00
“spicecat”
dd938a4943 improve dashboard badges display 2022-08-18 17:25:30 -04:00
“spicecat”
e742c0de50 improve dashboard badges display 2022-08-18 17:24:38 -04:00
Ani Simhadri
a61510ed8c config 2022-08-18 17:23:27 -04:00
fad8d34776
Merge pull request #30 from spicecat/search
Fixed buffet sections
2022-08-18 17:21:17 -04:00
45e21f21a2 Fixed buffet sections 2022-08-18 17:20:54 -04:00
“spicecat”
cc20217677 qa tags 2022-08-18 17:20:37 -04:00
“spicecat”
c9680903cb listquestion tags 2022-08-18 17:17:00 -04:00
“spicecat”
57e52c9a14 accepted answer chip 2022-08-18 17:12:00 -04:00
Ani Simhadri
9e4a0ffa8a Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:02:34 -04:00
“spicecat”
c328c5ca2d Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:02:19 -04:00
tejusk2
016f6b3ed2 schema fix 2022-08-18 16:02:09 -05:00
“spicecat”
f4228ad22e Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 17:02:05 -04:00
“spicecat”
0350ef8c08 fix search initalvalues 2022-08-18 17:02:02 -04:00
tejusk2
b4e6c6afb8 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 15:59:55 -05:00
Ani Simhadri
9d6903a183 config 2022-08-18 16:58:42 -04:00
tejusk2
4a24755a7f finished edit 2022-08-18 15:58:35 -05:00
“spicecat”
63da1e7cee mobile navbar userdata 2022-08-18 16:56:11 -04:00
77d5ff9a01 Fixed create question 2022-08-18 16:54:23 -04:00
9dcb13d516
Merge pull request #29 from spicecat/search
Search
2022-08-18 16:50:44 -04:00
4384db3df0
Merge branch 'dev' into search 2022-08-18 16:50:31 -04:00
426e68f446 Removed logs 2022-08-18 16:47:35 -04:00
40e072c99f Added tag to forms 2022-08-18 16:46:06 -04:00
tejusk2
7cf079bcae Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 15:35:17 -05:00
tejusk2
2479611245 edit vote beginning 2022-08-18 15:35:13 -05:00
“spicecat”
c7bf08de29 delete comment 2022-08-18 16:32:07 -04:00
cfb9e7a267 Added new search by tags 2022-08-18 16:27:00 -04:00
“spicecat”
0aa0674433 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 16:06:30 -04:00
“spicecat”
0538d5b6f6 warning fix 2022-08-18 16:06:27 -04:00
anirudhsimhadri
79c356c134 frontend for read status of mails 2022-08-18 16:04:18 -04:00
“spicecat”
19e12e25e9 remove print 2022-08-18 16:03:22 -04:00
“spicecat”
1eb58e1865 badge colors 2022-08-18 16:01:55 -04:00
“spicecat”
ed6ed93e2d fix badges 2022-08-18 15:51:28 -04:00
285a7ddbef Fixed badgecount 2022-08-18 15:19:46 -04:00
“spicecat”
bda650f728 update 2022-08-18 15:13:29 -04:00
b771d54453 Fixed badge 2022-08-18 15:07:36 -04:00
019877db9d Modified search 2022-08-18 15:07:15 -04:00
55514b408b Merge branch 'c4' into dev 2022-08-18 14:54:01 -04:00
92b219491b Fixed imports 2022-08-18 14:53:23 -04:00
30a35fce7f Added badgecount 2022-08-18 14:49:49 -04:00
5f389f5294 Completed badges 2022-08-18 14:35:05 -04:00
“spicecat”
424e14bbbd dashboard badges 2022-08-18 14:22:48 -04:00
48aec0a531 Completed badges 2022-08-18 14:20:46 -04:00
“spicecat”
fbd04488a8 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 14:20:19 -04:00
tejusk2
5ce82e4233 fixed merge conflict 2022-08-18 13:18:56 -05:00
tejusk2
c9321494ec Merge branch 'bounties' into dev 2022-08-18 13:18:40 -05:00
tejusk2
08dca22b7b UI for bounties 2022-08-18 13:15:40 -05:00
“spicecat”
0c4de60d8c badges template 2022-08-18 14:12:36 -04:00
“spicecat”
29e780eb90 finish change 10 2022-08-18 12:30:28 -04:00
anirudhsimhadri
3565120434 The Buffet view shows when a question has an accepted answer.
(Change 8)
2022-08-18 12:27:25 -04:00
tejusk2
2cf161c246 bounty logic 2022-08-18 10:48:02 -05:00
“spicecat”
55e47f3552 Merge branch 'dev' of https://github.com/spicecat/qOverflow into dev 2022-08-18 11:37:11 -04:00
“spicecat”
1a48027884 share answer button 2022-08-18 11:37:10 -04:00
52aa37c06c Added tags to createQuestion 2022-08-18 11:29:41 -04:00
ce92585eb7 Added changes 2022-08-18 11:21:31 -04:00
“spicecat”
5c955b7a88 share question 2022-08-18 11:07:55 -04:00
ea82c5dc85 Added mail branch fix 2022-08-18 11:00:52 -04:00
“spicecat”
82ed5cbf9a fix invalid mail form 2022-08-18 10:50:18 -04:00
08b5aee7af Send route returns 404 2022-08-18 10:40:55 -04:00
“spicecat”
83137b90d9 fix dashboard paper height 2022-08-18 10:18:55 -04:00
“spicecat”
e11077c772 fix refresh on send mail 2022-08-18 10:14:35 -04:00
“spicecat”
826d09277c fix refresh on add answer/comment 2022-08-18 10:12:13 -04:00
“spicecat”
c09dd06897 Merge branch 'main' of https://github.com/spicecat/qOverflow 2022-08-17 21:33:26 -04:00
“spicecat”
25efad7a1d move answer below comments 2022-08-17 21:32:44 -04:00
tejusk2
bc25ba146e Merge branch 'main' of https://github.com/spicecat/qOverflow 2022-08-17 20:30:11 -05:00
tejusk2
0e149a5ab3 delete user 2022-08-17 20:30:01 -05:00
“spicecat”
a7ffc886d3 reduce throttle and search bar all fields 2022-08-17 21:18:01 -04:00
f2536d3e92 Updated openapi yml 2022-08-17 21:11:17 -04:00
b6adfee731 Added delete route to router 2022-08-17 21:06:05 -04:00
fe227c3940 Added delete route 2022-08-17 21:05:00 -04:00
ae4e476ee3 Added concurrently 2022-08-17 20:50:46 -04:00
“spicecat”
76a1994248 Merge branch 'main' of https://github.com/spicecat/qOverflow 2022-08-17 20:44:36 -04:00
“spicecat”
bd7da9bed7 remove token on remember 2022-08-17 20:44:21 -04:00
anirudhsimhadri
83b7388dfb config 2022-08-17 20:25:30 -04:00
“spicecat”
1d86086b7e package scripts and update 2022-08-16 18:08:46 -05:00
tejusk2
1426fc8630 better mobile paginated elements? 2022-08-16 11:08:50 -05:00
124 changed files with 14915 additions and 11409 deletions

9
client/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
# syntax=docker/dockerfile:1
FROM node:16-alpine
WORKDIR /app
COPY ["package.json", "./"]
RUN apk update
RUN npm install
COPY . .
EXPOSE 3000/tcp
CMD npm start

View file

@ -1,56 +1,56 @@
{
"name": "qoverflow",
"version": "0.1.0",
"private": true,
"homepage": "",
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.9.1",
"acorn": "^8.8.0",
"crypto-js": "^4.1.1",
"dompurify": "^2.3.10",
"dotenv": "^16.0.1",
"formik": "^2.2.9",
"gh-pages": "^4.0.0",
"javascript-time-ago": "^2.5.6",
"js-cookie": "^3.0.1",
"marked": "^4.0.18",
"nvm": "^0.0.4",
"react": "^18.2.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"react-time-ago": "^7.2.1",
"remove-markdown": "^0.5.0",
"superagent": "^8.0.0",
"superagent-throttle": "^1.0.1",
"universal-cookie": "^4.0.4",
"yup": "^0.32.11"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"predeploy": "npm run build"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
"name": "client",
"version": "1.0.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.9.1",
"buffer": "^6.0.3",
"dompurify": "^2.3.10",
"dotenv": "^16.0.1",
"formik": "^2.2.9",
"gh-pages": "^4.0.0",
"javascript-time-ago": "^2.5.6",
"js-cookie": "^3.0.1",
"marked": "^4.0.18",
"react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"react-time-ago": "^7.2.1",
"superagent": "^8.0.0",
"superagent-throttle": "^1.0.1",
"universal-cookie": "^4.0.4",
"yup": "^0.32.11"
},
"scripts": {
"start": "react-scripts start",
"dev": "react-scripts start",
"build": "react-scripts build",
"predeploy": "npm run build",
"deploy": "gh-pages -b gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

@ -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 (
<Tooltip title={text} placement='bottom'>
<div>
<WorkspacePremiumIcon fontSize='large' style={{ color: color[rank] }} />
<Typography>{title}</Typography>
</div>
</Tooltip>
);
}
export default function Badges({ badges: { obtained, unobtained } }) {
const badgesGrid = (badges) => badges.map((badge) => (
<Grid item xs={2} sm={4} md={4} key={badge.title}>
<Badge {...{ ...badge }} />
</Grid>
))
return (
<>
<Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 4, sm: 8, md: 12 }}>
<Grid item xs={4} sm={8} md={12}>
<Typography variant='h5'>Obtained Badges</Typography>
</Grid>
{badgesGrid(obtained)}
</Grid>
<Grid container spacing={{ xs: 2, md: 3 }} columns={{ xs: 4, sm: 8, md: 12 }}>
<Grid item xs={4} sm={8} md={12}>
<Typography variant='h5'>Unobtained Badges</Typography>
</Grid>
{badgesGrid(unobtained)}
</Grid>
</>
);
}

View file

@ -1,8 +1,9 @@
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';
import ListQuestionInfo from './ListQuestionInfo';
import { useMediaQuery } from '@mui/material';
export default function ListQuestion({
question_id,
answers,
@ -10,23 +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 (
<span key={question_id}>
<ListItem disablePadding>
<Grid container>
<Grid item xs={2}>
<Stack justifyContent='center' sx={{ height: '100%' }}>
<Typography variant='body1'>{upvotes - downvotes} votes</Typography>
<Typography variant='body1'>{answers} answers</Typography>
<Typography variant='body1'>{views} views</Typography>
</Stack>
</Grid>
{(sm || md) && (
<Grid item xs={2}>
<Stack justifyContent='center' sx={{ height: '100%' }}>
<ListQuestionInfo
{...{
answers,
downvotes,
inline: false,
hasAcceptedAnswer,
tags,
upvotes,
views,
}}
/>
</Stack>
</Grid>
)}
<Grid item xs={10}>
{sm || md ? null : (
<ListQuestionInfo
{...{
answers,
downvotes,
inline: true,
hasAcceptedAnswer,
tags,
upvotes,
views,
}}
/>
)}
<CreationInfoTag {...{ createdAt, creator }} />
<Typography
variant='h6'
@ -34,11 +64,29 @@ export default function ListQuestion({
to={`/questions/${question_id}`}
style={{ textDecoration: 'none', color: 'inherit' }}
>
[{status}] {title}
[{status}] {title}{' '}
<Chip
color={hasAcceptedAnswer ? 'success' : 'error'}
label={hasAcceptedAnswer ? 'Accepted' : 'Unaccepted'}
size='small'
/>
</Typography>
<Typography noWrap variant='body1'>
{text.replace(/<[^>]*>?/gm, '')}
</Typography>
{tags.length > 0 && (
<Typography display='inline'>
Tags:{' '}
{tags.map((tag, index) => (
<Chip
label={tag}
size='small'
key={index}
sx={{ margin: '0 0.1vw' }}
/>
))}
</Typography>
)}
</Grid>
</Grid>
</ListItem>

View file

@ -0,0 +1,24 @@
import { Chip, Typography } from '@mui/material';
export default function ListQuestionInfo({
answers,
downvotes,
inline,
hasAcceptedAnswer,
tags,
upvotes,
views,
}) {
return inline ? (
<>
<Typography display='inline'>{upvotes - downvotes} votes | </Typography>
<Typography display='inline'>{answers} answers | </Typography>
<Typography display='inline'>{views} views</Typography>
</>
) : (
<>
<Typography display='block'>{upvotes - downvotes} votes</Typography>
<Typography display='block'>{answers} answers</Typography>
<Typography display='block'>{views} views</Typography>
</>
);
}

View file

@ -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 (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
onClick={() => { readMail(mail_id) }}
sx={read && { backgroundColor: 'lightGray' }}
>
<Box style={{ width: '100%' }} display={'flex'}>
<Typography>From {sender}</Typography>
<span style={{ width: '1vw' }} />

View file

@ -40,6 +40,16 @@ export default function MobileNavbar({ logout, userData, open, setOpen, mode, se
<ListItemText>Dashboard</ListItemText>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton disabled />
<ListItemText>
<Typography display='inline'><b>{userData?.level}</b></Typography>
<Typography display='inline' m={1}>{userData?.points}</Typography>
<Typography color='#ffd700' display='inline'>{userData?.badgeCount?.gold}</Typography>
/<Typography color='#c0c0c0' display='inline'>{userData?.badgeCount?.silver}</Typography>
/<Typography color='#cd7f32' display='inline'>{userData?.badgeCount?.bronze}</Typography>
</ListItemText>
</ListItem>
<ListItem>
<ListItemButton color='inherit' component={Link} to='/mail'>
<ListItemIcon>

View file

@ -7,7 +7,7 @@ export default function PaginatedList({ count, Component, data, handleChangePage
<Component {...item} key={index} />
))}
{count > 1 && (
<Box fullWidth display='flex' justifyContent='center' sx={{ margin: '1vh 0' }}>
<Box display='flex' justifyContent='center' sx={{ margin: '1vh 0' }}>
<Pagination
count={count}
onChange={handleChangePage}

View file

@ -2,15 +2,17 @@ import { IconButton, Typography } from '@mui/material';
import Gravatar from 'react-gravatar';
import { Link } from 'react-router-dom';
export default function Profile({ userData: { email, level, points } }) {
export default function Profile({ userData: { badgeCount, email, level, points } }) {
return (
<>
<IconButton component={Link} to='/dashboard'>
<Gravatar email={email} size={20} style={{ borderRadius: '100%' }} />
</IconButton>
<Typography variant='body1'>
<b>{level}</b> - {points}
</Typography>
<Typography display='inline'><b>{level}</b></Typography>
<Typography display='inline' m={1}>{points}</Typography>
<Typography color='#ffd700' display='inline'>{badgeCount?.gold}</Typography>
/<Typography color='#c0c0c0' display='inline'>{badgeCount?.silver}</Typography>
/<Typography color='#cd7f32' display='inline'>{badgeCount?.bronze}</Typography>
</>
);
}

View file

@ -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 (
<span key={answer_id}>
@ -47,12 +50,26 @@ export default function Answer({
<CreationInfoTag {...{ createdAt, creator, text: 'answered' }} />
<Markdown content={text} />
<CommentControl {...{ canComment, postComment }} />
<CopyToClipboard text={`${window.location.origin}/questions/${question_id}#${answer_id}`}>
<Button
color='inherit'
size='small'
m={1}
startIcon={<ShareIcon />}
style={{ textTransform: 'none' }}
variant='text'
>
Share
</Button>
</CopyToClipboard>
{canAccept && (
<span>
<Button
color='inherit'
size='small'
onClick={acceptAnswer}
style={{ marginLeft: '10px' }}
variant='standard'
style={{ textTransform: 'none' }}
variant='text'
>
Accept
</Button>

View file

@ -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}
</ListItemText>
<VoteControl {...{ downvotes, getVote, updateVote, upvotes }} />
{onDelete && <IconButton onClick={onDelete}>
<DeleteIcon />
</IconButton>}
</ListItem>
<Divider />
</span>

View file

@ -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 (
<span key={comment_id}>
@ -21,6 +23,9 @@ export default function Comment({
{text}
</ListItemText>
<VoteControl {...{ downvotes, getVote, updateVote, upvotes, creator }} />
{onDelete && <IconButton onClick={onDelete}>
<DeleteIcon />
</IconButton>}
</ListItem>
<Divider />
</span>

View file

@ -27,11 +27,12 @@ export default function CommentControl({
color='inherit'
disableRipple
onClick={toggleShow}
m={1}
size='small'
startIcon={<AddCommentOutlinedIcon />}
style={{ textTransform: 'none' }}
variant='text'
>
>
<Typography>Comment</Typography>
</Button>
</span>

View file

@ -5,7 +5,7 @@ import { AnswerForm } from 'controllers/FormControllers';
export default function CreateAnswer({ canAnswer, show, toggleShow }) {
return (
<div>
<div style = {{display : 'inline'}}>
<Tooltip
title={
canAnswer

View file

@ -7,12 +7,15 @@ import {
ListItemText,
Tooltip,
Typography,
TextField
} from '@mui/material';
import ShareIcon from '@mui/icons-material/Share';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { Link } from 'react-router-dom';
import { useState } from 'react';
import { Markdown } from 'components';
import { CreationInfoTag } from 'controllers';
import { CommentControl, CreateAnswer, VoteControl } from 'controllers/QAControllers';
import { CommentControl, VoteControl } from 'controllers/QAControllers';
const statusColor = (status) => {
switch (status) {
@ -28,6 +31,7 @@ const statusColor = (status) => {
};
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 (
<>
<Box m={2}>
<Typography variant='h4'>{title}</Typography>
@ -75,10 +93,12 @@ export default function Question({
label={hasAcceptedAnswer ? 'yes' : 'no'}
size='small'
/>
<Typography display='inline' m={1}>Tags: {tags.join(', ')}</Typography>
<br />
<Button
component={Link}
to='../ask'
style={{ marginLeft: '10px' }}
style={{ margin: '5px' }}
display='inline'
m={1}
variant='contained'
@ -90,9 +110,8 @@ export default function Question({
<span>
<Button
disabled={!canClose}
style={{ marginLeft: '10px' }}
style={{ margin: '5px' }}
display='inline'
m={1}
onClick={changeClose}
variant='contained'
>
@ -107,7 +126,7 @@ export default function Question({
<span>
<Button
disabled={!canProtect}
style={{ marginLeft: '10px' }}
style={{ margin: '5px' }}
display='inline'
m={1}
variant='contained'
@ -118,11 +137,54 @@ export default function Question({
</span>
</Tooltip>
<Tooltip
title={canBounty ? '' : 'You must be level 4 and this question must be open or protected'}
>
<span>
<Button
disabled={!canBounty}
style={{ margin: '5px' }}
display='inline'
m={1}
variant='contained'
onClick={handleBountyFix}
>
Add Bounty
</Button>
{show &&
<TextField value={value} size="small" type="number" inputProps={{ min, max }} disabled={!canBounty} label="bounty" onChange={(e) => {
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);
}
}} />
}
</span>
</Tooltip>
{ongoingVote.users.length > 0 && (
<Typography>
{ongoingVote.users.toString()} - voting to {ongoingVote.type} this question{' '}
</Typography>
)}
{hasBounty && (
<Typography>
there is a {hasBounty} point bounty on this question
</Typography>
)}
</Box>
<Divider />
@ -141,9 +203,20 @@ export default function Question({
<CreationInfoTag {...{ createdAt, creator }} />
<Markdown content={text} />
<CommentControl {...{ postComment, canComment }} />
<CopyToClipboard text={`${window.location.origin}/questions/${question_id}`}>
<Button
color='inherit'
size='small'
m={1}
startIcon={<ShareIcon />}
style={{ textTransform: 'none' }}
variant='text'
>
Share
</Button>
</CopyToClipboard>
</ListItemText>
</ListItem>
<CreateAnswer />
</>
);
}

View file

@ -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 (
<div style = {{display : 'inline', marginLeft : '10px'}}>
<Tooltip
title= {
canSuggest
? ''
: 'You need to be level 7'}
>
<span>
<Button disabled={!canSuggest} variant='contained' onClick={toggleShow}>
Add Suggestion
</Button>
</span>
</Tooltip>
{show && (
<Card>
<CardContent>
<EditFormController />
</CardContent>
</Card>
)}
{ongoingEdit.users.length > 0 &&
<Card>
<CardContent>
New Title: <Markdown {...{content: ongoingEdit.new[1]}}/>
New Text: <Markdown {...{content: ongoingEdit.new[0]}}/>
</CardContent>
<Typography> {ongoingEdit.users} has voted to edit the question </Typography>
</Card>
}
</div>
);
}

View file

@ -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,

View file

@ -6,68 +6,56 @@ 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 [loading, setLoading] = useState(true);
const [, setSearchParams] = useSearchParams();
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,
});
const getData = async ({ question_id }) => {
const { questions } = await searchQuestions({
sort,
});
setLoading(() => false);
return questions;
};
setLoading(() => false);
return questions;
};
const handleSortChange = (_, newSort) => {
setSearchParams(sortObjArr[newSort]);
setSort(newSort);
};
const handleSortChange = (_, value) => {
setSearchParams({ sort: value });
setSort(value);
};
return (
<Box height='100%'>
<Box
style={{
width: '100%',
border: '3px lightGray solid',
textAlign: 'center',
padding: '1vh',
}}
>
<Typography variant='h3'>Top Questions</Typography>
return (
<Box height='100%'>
<Box
style={{
width: '100%',
border: '3px lightGray solid',
textAlign: 'center',
padding: '1vh',
}}
>
<Typography variant='h3'>Top Questions</Typography>
<Button
variant='contained'
style={{ marginTop: '1%' }}
component={Link}
to='questions/ask'
>
Ask A Question
</Button>
<Button variant='contained' style={{ marginTop: '1%' }} component={Link} to='questions/ask'>
Ask A Question
</Button>
<ToggleButtonGroup
color='primary'
value={sort}
exclusive
onChange={handleSortChange}
style={{ display: 'block', marginTop: '1%' }}
>
<ToggleButton value={0}>Recent</ToggleButton>
<ToggleButton value={1}>Best</ToggleButton>
<ToggleButton value={2}>Interesting</ToggleButton>
<ToggleButton value={3}>Hot</ToggleButton>
</ToggleButtonGroup>
</Box>
{loading && <LoadingBar />}
<PaginatedList {...{ concat: true, Component: ListQuestion, getData }} />
</Box>
);
<ToggleButtonGroup
color='primary'
value={sort}
exclusive
onChange={handleSortChange}
style={{ display: 'block', marginTop: '1%' }}
>
<ToggleButton value=''>Recent</ToggleButton>
<ToggleButton value='u'>Best</ToggleButton>
<ToggleButton value='uvc'>Interesting</ToggleButton>
<ToggleButton value='uvac'>Hot</ToggleButton>
</ToggleButtonGroup>
</Box>
{loading && <LoadingBar />}
<PaginatedList {...{ Component: ListQuestion, getData }} />
</Box>
);
}

View file

@ -1,4 +1,5 @@
import {
Alert,
Box,
Button,
Grid,
@ -15,14 +16,17 @@ import { Link, useNavigate } from 'react-router-dom';
import { ListAnswer, ListQuestion, LoadingBar } from 'components';
import { useUser } from 'contexts';
import { PaginatedList } from 'controllers';
import { getUserAnswers, getUserQuestions } from 'services/userServices';
import { Badges, PaginatedList } from 'controllers';
import { deleteUser, getUserAnswers, getUserQuestions } from 'services/userServices';
export default function Dashboard() {
const navigate = useNavigate();
const [current, setCurrent] = useState('questions');
const [loading, setLoading] = useState(true);
const [confirmDelete, setConfirmDelete] = useState(false)
const {
userData: { email, level, points, username },
} = useUser();
@ -57,6 +61,20 @@ export default function Dashboard() {
if (!error) return questions;
};
const deleteCurrentUser = () => {
if (confirmDelete) {
deleteUser();
navigate('/users/login')
setConfirmDelete(false);
} else {
setConfirmDelete(true);
setTimeout(cancelDelete, 30000)
}
}
function cancelDelete() {
setConfirmDelete(false)
}
return (
<>
<Helmet>
@ -66,23 +84,26 @@ export default function Dashboard() {
sx={{
margin: { xs: '0.3vh', sm: '1vh', md: '1vh' },
padding: { xs: '1vh', sm: '3vh', md: '5vh' },
height: '100%',
}}
>
<Grid
container
spacing={2}
sx={{
display: { xs: 'flex', sm: 'block', md: 'block' },
justifyContent: 'center',
}}
// sx={{
// display: { xs: 'flex', sm: 'block', md: 'block' },
// justifyContent: 'center',
// }}
>
<Grid item sm={4} md={2}>
<Grid item sm={4} md={3}>
{email && (
<Gravatar email={email} size={200} style={{ borderRadius: '100%' }} />
)}
</Grid>
<Grid item sm={12} md={8}>
<Badges />
</Grid>
<Grid item sm={8} md={10}>
{confirmDelete && <Alert variant="warning">Click delete button again to confirm delete, cancelling in 30 seconds</Alert>}
<Typography>Username: {username}</Typography>
<Typography>Email: {email}</Typography>
<Typography>Level: {level}</Typography>
@ -106,6 +127,17 @@ export default function Dashboard() {
>
Update User Information
</Button>
<Button
color='warning'
onClick={deleteCurrentUser}
sx={{ margin: '0 1vh' }}
variant='outlined'
fullWidth
>
Delete User
</Button>
<ToggleButtonGroup
value={current}
exclusive

View file

@ -2,7 +2,7 @@ import { Alert, Card, CardContent, Grid, Typography } from '@mui/material';
import { Helmet } from 'react-helmet';
import { Link, useLocation } from 'react-router-dom';
import { LoginForm } from '../controllers/FormControllers';
import { LoginForm } from 'controllers/FormControllers';
export default function Login() {
let location = useLocation();

View file

@ -1,5 +1,6 @@
import { List, ListItem } from '@mui/material';
import { AnswersList, CommentsList, Question } from 'controllers/QAControllers';
import Suggest from 'components/QAComponents/Suggest';
import { AnswersList, CommentsList, CreateAnswer, Question } from 'controllers/QAControllers';
export default function QA() {
return (
@ -8,6 +9,9 @@ export default function QA() {
<ListItem sx={{ pl: 4 }}>
<CommentsList />
</ListItem>
<CreateAnswer/>
<Suggest/>
<div id='answersList' />
<AnswersList />
</List>
);

View file

@ -1,4 +1,5 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { remember } from 'services/userServices';
@ -7,12 +8,14 @@ const initialUserData = { loading: true };
const UserContext = createContext();
export default function UserProvider({ children }) {
const [userData, setUserData] = useState(initialUserData);
const [userData, setUserData] = useState(initialUserData)
const navigate = useNavigate();
useEffect(() => {
const loadUserData = async () => {
const { user } = await remember();
setUserData(user || { loading: false });
const { user, error } = await remember();
if (error) navigate('/users/login')
else setUserData(user || { loading: false });
};
if (userData.loading) loadUserData();
}, [userData.loading]);

View file

@ -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 })
);
}

View file

@ -1,5 +1,3 @@
import { useNavigate } from 'react-router-dom';
import { useQuestion, useUser } from 'contexts';
import { Form } from 'controllers/FormControllers';
import { answerFields } from 'services/fields';
@ -7,8 +5,6 @@ import { postAnswer } from 'services/questionsServices';
import { answerSchema } from 'services/schemas';
export default function AnswerFormController({ toggleShow }) {
const navigate = useNavigate();
const {
questionData: { question_id },
} = useQuestion();
@ -19,7 +15,7 @@ export default function AnswerFormController({ toggleShow }) {
const answerQuestion = async (fields) => {
await postAnswer(question_id, { creator: username, text: fields.text });
toggleShow();
navigate('');
window.location.reload(false);
};
return Form({

View file

@ -12,7 +12,7 @@ export default function AskFormController() {
const { status } = await postQuestion(fields);
if (status === 200) {
navigate('../');
navigate('/');
}
};

View file

@ -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}
});
}

View file

@ -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,

View file

@ -23,13 +23,18 @@ export default function SendMailController() {
}, []);
const sendMail = async (fields) => {
const { error } = await postMail(fields);
if (!error) navigate('');
const { error, status } = await postMail(fields);
if (error) {
if (status === 500) return { receiver: 'Error' };
else if (status === 404) return { receiver: 'User not found' };
} else {
window.location.reload(false);
}
};
return Form({
fields: composeMailFields,
onSubmit: sendMail,
validate: sendMail,
validationSchema: mailSchema,
});
}

View file

@ -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);
};

View file

@ -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 <AnswerComment {...{ ...props, getVote, updateVote }} />;
return <AnswerComment {...{ getVote, onDelete, updateVote, ...props }} />;
}

View file

@ -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 (
<Answer
@ -22,8 +20,8 @@ export default function AnswerController({ answer_id, question_id, ...props }) {
updateVote,
postComment,
acceptAnswer,
question_id,
answer_id,
question_id,
}}
/>
);

View file

@ -1,5 +1,4 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { CommentControl } from 'components/QAComponents';
import { commentFields } from 'services/fields';
@ -7,7 +6,6 @@ import { commentSchema } from 'services/schemas';
export default function CommentControlController({ postComment, canComment }) {
const [show, setShow] = useState(false);
const navigate = useNavigate();
const toggleShow = () => {
setShow(!show);
@ -17,7 +15,7 @@ export default function CommentControlController({ postComment, canComment }) {
await postComment(data);
toggleShow();
navigate('');
window.location.reload(false);
};
return CommentControl({

View file

@ -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 <Comment {...{ getVote, updateVote, ...props }} />;
return <Comment {...{ getVote, onDelete, updateVote, ...props }} />;
}

View file

@ -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) => <Answer {...{ ...props, canComment, canAccept }} />;
return !loading && <PaginatedList {...{ count, Component: NewAnswer, getData }} />;
return !loading && <PaginatedList {...{ count, Component: NewAnswer, getData, scroll: true }} />;
}
export function CommentsList() {

View file

@ -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)
}
}
@ -77,8 +96,6 @@ export default function QuestionController() {
const updateVote = (data) => updateQuestionVote(question_id, data);
const postComment = async (data) => {
const { comment } = await postQuestionComment(question_id, data);
navigate('');
return comment;
};
@ -95,6 +112,9 @@ export default function QuestionController() {
getVote,
updateVote,
postComment,
show,
handleBounty
}}
/>
)}

View file

@ -19,7 +19,9 @@ export default function SearchBarController() {
} else {
navigate({
pathname: '/questions/search',
search: `?${createSearchParams({ title: search })}`,
search: `?${createSearchParams({
title: search,
})}`,
replace: true,
});
}

View file

@ -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;

View file

@ -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 };

View file

@ -4,7 +4,7 @@ import Throttle from 'superagent-throttle';
const throttle = new Throttle({
active: true,
rate: 2,
rate: 10,
ratePer: 1000,
concurrent: 2,
});

View file

@ -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
};

View file

@ -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;
}

View file

@ -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
}

View file

@ -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 };

View file

@ -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
};

View file

@ -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
};

View file

@ -6,45 +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 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 });
@ -53,23 +57,33 @@ const updateUser = async (data) => callUsersAPI('patch', ``, data);
const register = async (data) => callUsersAPI('post', ``, data);
const remember = async () => callUsersAPI('get', `/remember`);
const remember = async () => {
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);
const resetPassword = async (id, data) => callUsersAPI('post', `/reset/${id}`, data);
export {
getLoginAttempts,
getUser,
getUserAnswers,
getUserQuestions,
incrementLoginAttempts,
login,
logout,
register,
remember,
updateUser,
requestReset,
resetPassword,
getLoginAttempts,
getUser,
getUserAnswers,
getUserBadges,
getUserQuestions,
incrementLoginAttempts,
login,
logout,
register,
remember,
updateUser,
requestReset,
resetPassword,
deleteUser
};

56
docker-compose.yml Normal file
View file

@ -0,0 +1,56 @@
version: '3.8'
services:
client:
restart: unless-stopped
build:
context: ./client
ports:
- 3000:3000
volumes:
- ./client:/app
- client-node:/app/node_modules
networks:
- qOverflow
env_file: ./client/.env
environment:
WATCHPACK_POLLING: true
depends_on:
- server
server:
restart: unless-stopped
build:
context: ./server
ports:
- 3001:3001
volumes:
- ./server:/app
- server-node:/app/node_modules
networks:
- qOverflow
env_file: ./server/.env
environment:
DB_CONN_STRING: 'mongodb://mongodb:27017'
CHOKIDAR_USEPOLLING: true
depends_on:
- mongodb
mongodb:
restart: unless-stopped
image: mongo:5
ports:
- 27017:27017
volumes:
- data:/data/db
- db-config:/data/configdb
networks:
- qOverflow
logging:
driver: none
volumes:
data:
db-config:
client-node:
server-node:
networks:
qOverflow: {}

19177
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,21 @@
{
"name": "qoverflow",
"version": "1.0.0",
"description": "",
"description": "qOverflow client and server",
"main": "index.js",
"homepage": "https://github.com/spicecat/qOverflow#readme",
"homepage": "https://github.com/spicecat/qOverflow",
"scripts": {
"start": "npm start -w",
"start": "concurrently \"npm start -w client\" \"npm start -w server\"",
"start:server": "npm start -w server",
"start:client": "npm start -w client",
"start:prod": "npm run prod -w server",
"prestart:prod": "npm ci && npm run build",
"dev": "npm start -workspaces",
"build": "npm run build -w client"
"build": "npm run build -w client",
"build:package": "tar -czf dist.tar.gz client node_modules server LICENSE package-lock.json package.json",
"dist": "npm run build:package",
"predeploy": "npm run predeploy -w client",
"deploy": "npm run deploy -w client",
"predist": "npm ci && npm run build"
},
"repository": {
"type": "git",
@ -25,5 +29,11 @@
"workspaces": [
"client",
"server"
]
],
"devDependencies": {
"concurrently": "^7.3.0"
},
"dependencies": {
"react-copy-to-clipboard": "^5.1.0"
}
}

8
server/Dockerfile Normal file
View file

@ -0,0 +1,8 @@
# syntax=docker/dockerfile:1
FROM node:16-alpine
WORKDIR /app
COPY ["package.json", "./"]
RUN npm install
COPY . .
EXPOSE 3001/tcp
CMD npm run start

64
server/badges.json Normal file
View file

@ -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"
}
]
}

View file

@ -4,13 +4,13 @@
* Module dependencies.
*/
const app = require('server/app');
const app = require('../app');
const debug = require('debug')('server:server');
const http = require('http');
const mongoose = require('mongoose');
const cron = require('node-cron');
const { refreshQuestions, refreshUsers } = require('server/utils/cronRefresh');
const { refreshQuestions, refreshUsers } = require('../utils/cronRefresh');
require('dotenv').config();

View file

@ -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);

View file

@ -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);

View file

@ -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 }
);

View file

@ -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);

View file

@ -1,4 +1,5 @@
const mongoose = require('mongoose');
const calculateUserBadges = require('../../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);

View file

@ -1,7 +1,7 @@
const deriveKeyFromPassword = require('server/utils/auth');
const createRequest = require('server/utils/api');
const config = require('server/config.json');
const User = require('server/db/models/User');
const deriveKeyFromPassword = require('../utils/auth');
const createRequest = require('../utils/api');
const config = require('../config.json');
const User = require('../db/models/User');
async function basicAuth(req, res, next) {
const authHeader = req.get('Authorization');

View file

@ -1,6 +1,6 @@
const Token = require('server/db/models/Token');
const User = require('server/db/models/User');
const config = require('server/config.json');
const Token = require('../db/models/Token');
const User = require('../db/models/User');
const config = require('../config.json');
async function tokenAuth(req, res, next) {
const authHeader = req.get('Authorization');

View file

@ -194,6 +194,22 @@ paths:
$ref: '#/components/responses/ServerError'
tags:
- users
/users/delete:
delete:
summary: Permanently delete a user's account
security:
- BearerAuth: []
responses:
'200':
description: OK
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/ServerError'
tags:
- users
/users/reset:
post:
summary: Get a link for the user to reset their password

View file

@ -1,18 +1,16 @@
{
"name": "server",
"version": "0.0.0",
"version": "1.0.0",
"private": true,
"dependencies": {
"cookie-parser": "~1.4.6",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"debug": "~4.3.4",
"debug": "^4.3.4",
"dotenv": "^16.0.1",
"express": "~4.18.1",
"http-errors": "~2.0.0",
"jade": "^1.9.2",
"express": "^4.18.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.4.5",
"morgan": "~1.10.0",
"morgan": "^1.10.0",
"node-cron": "^3.0.1",
"pm2": "^5.2.0",
"superagent": "^8.0.0",
@ -23,7 +21,6 @@
"prod": "pm2 start ./bin/www"
},
"devDependencies": {
"jest": "^28.1.3",
"nodemon": "^2.0.19",
"supertest": "^6.2.4"
}

View file

@ -1,11 +0,0 @@
{
"files": {
"main.js": "/static/js/main.7610eaba.js",
"static/media/bdpa-logo.svg": "/static/media/bdpa-logo.c32c2f3a895925461fe81028fecc5c45.svg",
"index.html": "/index.html",
"main.7610eaba.js.map": "/static/js/main.7610eaba.js.map"
},
"entrypoints": [
"static/js/main.7610eaba.js"
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -1 +0,0 @@
<!doctype html><html lang="en" dir="ltr"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><meta name="theme-color" content="#000000"/><meta name="description" content="qOverflow"/><link rel="manifest" href="/manifest.json"/><title>qOverflow</title><script defer="defer" src="/static/js/main.7610eaba.js"></script></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

View file

@ -1,15 +0,0 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

File diff suppressed because one or more lines are too long

View file

@ -1,112 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
* Determine if an object is a Buffer
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! js-cookie v3.0.1 | MIT */
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */
/**
* @license React
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react-jsx-runtime.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* React Router DOM v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/**
* React Router v6.3.0
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
/** @license MUI v5.8.7
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v16.13.1
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 78 KiB

View file

@ -1,12 +1,14 @@
const express = require('express');
const router = express.Router();
const tokenAuth = require('server/middleware/tokenAuth');
const tokenAuth = require('../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;

View file

@ -1,21 +1,19 @@
const config = require('server/config.json');
const Mail = require('server/db/models/Mail');
const { getAllMail } = require('server/utils/getData');
const config = require('../../config.json');
const Mail = require('../../db/models/Mail');
const { getAllMail } = require('../../utils/getData');
async function Get(req, res) {
const { lastMailFetch, username } = req.user;
// Fetch mail if expired
if (Number(lastMailFetch) + config.mailExpires < Date.now()) {
const messages = await getAllMail({ username });
return res.send({ messages });
} else {
const messages = await Mail.find({ receiver: username }).sort({
createdAt: 'desc',
});
return res.send({ messages });
await getAllMail({ username });
}
const messages = await Mail.find({ receiver: username }).sort({
createdAt: 'desc',
});
return res.send({ messages });
}
module.exports = Get;

View file

@ -0,0 +1,12 @@
const Mail = require('../../db/models/Mail');
const config = require('../../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;

View file

@ -1,6 +1,7 @@
const Mail = require('server/db/models/Mail');
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const Mail = require('../../db/models/Mail');
const User = require('../../db/models/User');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
async function Send(req, res) {
const { username } = req.user;
@ -11,6 +12,12 @@ async function Send(req, res) {
return res.status(400).send(config.errorIncomplete);
}
const cachedReciever = await User.findOne({ username: receiver });
if (!cachedReciever) {
return res.status(404).send(config.errorNotFound);
}
const { success, message } = await createRequest('post', `/mail`, {
sender: username,
receiver,

View file

@ -1,8 +1,7 @@
const express = require('express');
const router = express.Router();
const tokenAuth = require('server/middleware/tokenAuth');
const tokenAuth = require('../middleware/tokenAuth');
const CreateAnswer = require('./question/CreateAnswer');
const CreateAnswerComment = require('./question/CreateAnswerComment');
@ -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;

View file

@ -1,9 +1,9 @@
const config = require('server/config.json');
const Answer = require('server/db/models/Answer');
const User = require('server/db/models/User');
const { getQuestion } = require('server/utils/question');
const getUserLevel = require('server/utils/getUserLevel');
const createRequest = require('server/utils/api');
const config = require('../../config.json');
const Answer = require('../../db/models/Answer');
const User = require('../../db/models/User');
const { getQuestion } = require('../../utils/question');
const getUserLevel = require('../../utils/getUserLevel');
const createRequest = require('../../utils/api');
async function CreateAnswer(req, res) {
const { user } = req;
@ -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);
}

View file

@ -1,9 +1,9 @@
const config = require('server/config.json');
const Answer = require('server/db/models/Answer');
const Comment = require('server/db/models/Comment');
const { getQuestion } = require('server/utils/question');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const config = require('../../config.json');
const Answer = require('../../db/models/Answer');
const Comment = require('../../db/models/Comment');
const { getQuestion } = require('../../utils/question');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
async function CreateAnswerComment(req, res) {
const user = req.user;

View file

@ -1,8 +1,8 @@
const config = require('server/config.json');
const Comment = require('server/db/models/Comment');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const config = require('../../config.json');
const Comment = require('../../db/models/Comment');
const { getQuestion, refreshQuestion } = require('../../utils/question');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
async function CreateComment(req, res) {
const { user } = req;

View file

@ -1,14 +1,18 @@
const config = require('server/config.json');
const Question = require('server/db/models/Question');
const User = require('server/db/models/User');
const createRequest = require('server/utils/api');
const config = require('../../config.json');
const Question = require('../../db/models/Question');
const User = require('../../db/models/User');
const createRequest = require('../../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`, {

View file

@ -1,6 +1,6 @@
const Comment = require('server/db/models/Comment');
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const Comment = require('../../db/models/Comment');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
async function DeleteAnswerComment(req, res) {
const { user } = req;

View file

@ -1,6 +1,6 @@
const Comment = require('server/db/models/Comment');
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const Comment = require('../../db/models/Comment');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
async function DeleteComment(req, res) {
const { user } = req;

View file

@ -1,6 +1,6 @@
const Answer = require('server/db/models/Answer');
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const Answer = require('../../db/models/Answer');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
async function EditAnswer(req, res) {
const { user } = req;
@ -10,10 +10,7 @@ async function EditAnswer(req, res) {
if (!text) return res.status(400).send(config.errorIncomplete);
// Retrieve latest question data
const answerPull = await createRequest(
'get',
`/questions/${question_id}/answers/${answer_id}`
);
const answerPull = await createRequest('get', `/questions/${question_id}/answers/${answer_id}`);
if (!answerPull.success) return res.status(500).send(config.errorGeneric);

View file

@ -1,9 +1,9 @@
const config = require('server/config.json');
const Answer = require('server/db/models/Answer');
const Question = require('server/db/models/Question');
const User = require('server/db/models/User');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const createRequest = require('server/utils/api');
const config = require('../../config.json');
const Answer = require('../../db/models/Answer');
const Question = require('../../db/models/Question');
const User = require('../../db/models/User');
const { getQuestion, refreshQuestion } = require('../../utils/question');
const createRequest = require('../../utils/api');
async function EditAnswerAccepted(req, res) {
const { user } = req;
@ -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);
}

View file

@ -1,7 +1,7 @@
const createRequest = require('server/utils/api');
const config = require('server/config.json');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const createRequest = require('../../utils/api');
const config = require('../../config.json');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
async function EditAnswerCommentVote(req, res) {
const { user } = req;
@ -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);
}

View file

@ -1,8 +1,8 @@
const createRequest = require('server/utils/api');
const config = require('server/config.json');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const Answer = require('server/db/models/Answer');
const createRequest = require('../../utils/api');
const config = require('../../config.json');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
const Answer = require('../../db/models/Answer');
async function EditAnswerVote(req, res) {
const { user } = req;
@ -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' });
}

View file

@ -1,7 +1,7 @@
const createRequest = require('server/utils/api');
const config = require('server/config.json');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const createRequest = require('../../utils/api');
const config = require('../../config.json');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
async function EditAnswerVote(req, res) {
const { user } = req;
@ -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);
}

View file

@ -1,26 +1,20 @@
const config = require('server/config.json');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const createRequest = require('server/utils/api');
const config = require('../../config.json');
const { getQuestion, refreshQuestion } = require('../../utils/question');
const createRequest = require('../../utils/api');
async function EditQuestion(req, res) {
const { user } = req;
const { text } = req.body;
const {views} = req.body;
const { views } = req.body;
const { question_id } = req.params;
if(views){
const { success } = await createRequest(
'patch',
`/questions/${question_id}`,
{ views }
);
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);
@ -31,16 +25,12 @@ async function EditQuestion(req, res) {
if (question.creator !== user.username) return res.status(403).send(config.errorForbidden);
// Patch question with BDPA server
const { success } = await createRequest(
'patch',
`/questions/${question_id}`,
{ text }
);
const { success } = await createRequest('patch', `/questions/${question_id}`, { text });
if (!success) return res.status(500).send(config.errorGeneric);
await refreshQuestion(question_id);
return res.sendStatus(200);
}

View file

@ -0,0 +1,67 @@
const config = require('../../config.json');
const Question = require('../../db/models/Question');
const { getQuestion, refreshQuestion } = require('../../utils/question');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../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;

View file

@ -0,0 +1,31 @@
const Question = require('../../db/models/Question');
const User = require('../../db/models/User');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
const config = require('../../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;

View file

@ -1,15 +1,15 @@
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');
const config = require('../../config.json');
const Question = require('../../db/models/Question');
const { getQuestion, refreshQuestion } = require('../../utils/question');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
async function EditQuestionStatusClosed(req, res) {
const { user } = req;
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

View file

@ -1,15 +1,15 @@
const config = require('server/config.json');
const Question = require('server/db/models/Question');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const getUserLevel = require('server/utils/getUserLevel');
const createRequest = require('server/utils/api');
const config = require('../../config.json');
const Question = require('../../db/models/Question');
const { getQuestion, refreshQuestion } = require('../../utils/question');
const getUserLevel = require('../../utils/getUserLevel');
const createRequest = require('../../utils/api');
async function EditQuestionStatusProtected(req, res) {
const { user } = req;
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

View file

@ -1,15 +1,15 @@
const config = require('server/config.json');
const Question = require('server/db/models/Question');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const getUserLevel = require('server/utils/getUserLevel');
const createRequest = require('server/utils/api');
const config = require('../../config.json');
const Question = require('../../db/models/Question');
const { getQuestion } = require('../../utils/question');
const getUserLevel = require('../../utils/getUserLevel');
const createRequest = require('../../utils/api');
async function EditQuestionStatusReopened(req, res) {
const { user } = req;
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

View file

@ -1,9 +1,9 @@
const config = require('server/config.json');
const User = require('server/db/models/User');
const Vote = require('server/db/models/Vote');
const Question = require('server/db/models/Question');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const config = require('../../config.json');
const User = require('../../db/models/User');
const Vote = require('../../db/models/Vote');
const Question = require('../../db/models/Question');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
async function upvote(res, user) {}
@ -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`, {

View file

@ -1,7 +1,7 @@
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
async function GetAnswerCommentVote(req, res) {
const { user } = req;
@ -19,8 +19,9 @@ async function GetAnswerCommentVote(req, res) {
parent_id: comment_id,
creator: user.username,
});
} catch { return res.status(400).send(config.errorNotFound); }
} catch {
return res.status(400).send(config.errorNotFound);
}
if (cachedVote) return res.send({ vote: cachedVote.status });

View file

@ -1,7 +1,7 @@
const config = require('server/config.json');
const Comment = require('server/db/models/Comment');
const { getAllComments } = require('server/utils/getData');
const { getAnswer } = require('server/utils/question');
const config = require('../../config.json');
const Comment = require('../../db/models/Comment');
const { getAllComments } = require('../../utils/getData');
const { getAnswer } = require('../../utils/question');
async function GetAnswerComments(req, res) {
const { question_id, answer_id } = req.params;

View file

@ -1,15 +1,14 @@
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
async function GetAnswerVote(req, res) {
const { user } = req;
const { question_id, answer_id } = req.params;
// Verify user has required level
if (getUserLevel(user.points) < 2)
return res.send({ vote: null });
if (getUserLevel(user.points) < 2) return res.send({ vote: null });
let cachedVote;
try {
@ -18,8 +17,9 @@ async function GetAnswerVote(req, res) {
parent_id: answer_id,
creator: user.username,
});
} catch { return res.status(400).send(config.errorNotFound); }
} catch {
return res.status(400).send(config.errorNotFound);
}
if (cachedVote) return res.send({ vote: cachedVote.status });

View file

@ -1,7 +1,7 @@
const config = require('server/config.json');
const Answer = require('server/db/models/Answer');
const { getAllAnswers } = require('server/utils/getData');
const { getQuestion } = require('server/utils/question');
const config = require('../../config.json');
const Answer = require('../../db/models/Answer');
const { getAllAnswers } = require('../../utils/getData');
const { getQuestion } = require('../../utils/question');
async function GetAnswers(req, res) {
const { question_id } = req.params;

View file

@ -1,15 +1,14 @@
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
async function GetCommentVote(req, res) {
const { user } = req;
const { question_id, comment_id } = req.params;
// Verify user has level required to vote
if (getUserLevel(user.points) < 2)
return res.send({ vote: null });
if (getUserLevel(user.points) < 2) return res.send({ vote: null });
let cachedVote;
try {
@ -18,7 +17,9 @@ async function GetCommentVote(req, res) {
parent_id: comment_id,
creator: user.username,
});
} catch { return res.status(400).send(config.errorNotFound); }
} catch {
return res.status(400).send(config.errorNotFound);
}
if (cachedVote) return res.send({ vote: cachedVote.status });

View file

@ -1,7 +1,7 @@
const config = require('server/config.json');
const Comment = require('server/db/models/Comment');
const { getAllComments } = require('server/utils/getData');
const { getQuestion } = require('server/utils/question');
const config = require('../../config.json');
const Comment = require('../../db/models/Comment');
const { getAllComments } = require('../../utils/getData');
const { getQuestion } = require('../../utils/question');
async function GetComments(req, res) {
const { question_id } = req.params;

View file

@ -1,5 +1,5 @@
const config = require('server/config.json');
const { getQuestion } = require('server/utils/question');
const config = require('../../config.json');
const { getQuestion } = require('../../utils/question');
async function GetQuestion(req, res) {
const { question_id } = req.params;

View file

@ -1,15 +1,14 @@
const config = require('server/config.json');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
const Vote = require('server/db/models/Vote');
const config = require('../../config.json');
const createRequest = require('../../utils/api');
const getUserLevel = require('../../utils/getUserLevel');
const Vote = require('../../db/models/Vote');
async function GetQuestionVote(req, res) {
const { user } = req;
const { question_id } = req.params;
// Verify user has level required to vote
if (getUserLevel(user.points) < 2)
return res.send({ vote: null });
if (getUserLevel(user.points) < 2) return res.send({ vote: null });
let cachedVote;
try {
@ -18,8 +17,9 @@ async function GetQuestionVote(req, res) {
parent_id: question_id,
creator: user.username,
});
} catch { return res.status(400).send(config.errorNotFound); }
} catch {
return res.status(400).send(config.errorNotFound);
}
if (cachedVote) return res.send({ vote: cachedVote.status });

Some files were not shown because too many files have changed in this diff Show more