commit
2fb612f139
74 changed files with 1234 additions and 331 deletions
|
|
@ -16,6 +16,7 @@
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"marked": "^4.0.18",
|
"marked": "^4.0.18",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-gravatar": "^2.6.3",
|
"react-gravatar": "^2.6.3",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
|
|
|
||||||
47
client/src/components/Badges.jsx
Normal file
47
client/src/components/Badges.jsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Divider, Grid, ListItem, Stack, Typography } from '@mui/material';
|
import { Divider, Grid, ListItem, Stack, Typography, Chip } from '@mui/material';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { CreationInfoTag } from 'controllers';
|
import { CreationInfoTag } from 'controllers';
|
||||||
|
|
@ -11,24 +11,52 @@ export default function ListQuestion({
|
||||||
creator,
|
creator,
|
||||||
downvotes,
|
downvotes,
|
||||||
status,
|
status,
|
||||||
|
tags,
|
||||||
text,
|
text,
|
||||||
title,
|
title,
|
||||||
upvotes,
|
upvotes,
|
||||||
views,
|
views,
|
||||||
|
hasAcceptedAnswer,
|
||||||
}) {
|
}) {
|
||||||
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
|
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
|
||||||
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
|
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span key={question_id}>
|
<span key={question_id}>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<Grid container>
|
<Grid container>
|
||||||
<Grid item xs={2}>
|
{(sm || md) && (
|
||||||
<Stack justifyContent='center' sx={{ height: '100%' }}>
|
<Grid item xs={2}>
|
||||||
{sm || md ? <ListQuestionInfo {...{upvotes, downvotes, answers, views, inline: false}}/> : null}
|
<Stack justifyContent='center' sx={{ height: '100%' }}>
|
||||||
</Stack>
|
<ListQuestionInfo
|
||||||
</Grid>
|
{...{
|
||||||
|
answers,
|
||||||
|
downvotes,
|
||||||
|
inline: false,
|
||||||
|
hasAcceptedAnswer,
|
||||||
|
tags,
|
||||||
|
upvotes,
|
||||||
|
views,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid item xs={10}>
|
<Grid item xs={10}>
|
||||||
{sm || md ? null: <ListQuestionInfo {...{upvotes, downvotes, answers, views, inline: true}}/>}
|
{sm || md ? null : (
|
||||||
|
<ListQuestionInfo
|
||||||
|
{...{
|
||||||
|
answers,
|
||||||
|
downvotes,
|
||||||
|
inline: true,
|
||||||
|
hasAcceptedAnswer,
|
||||||
|
tags,
|
||||||
|
upvotes,
|
||||||
|
views,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<CreationInfoTag {...{ createdAt, creator }} />
|
<CreationInfoTag {...{ createdAt, creator }} />
|
||||||
<Typography
|
<Typography
|
||||||
variant='h6'
|
variant='h6'
|
||||||
|
|
@ -36,11 +64,29 @@ export default function ListQuestion({
|
||||||
to={`/questions/${question_id}`}
|
to={`/questions/${question_id}`}
|
||||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||||
>
|
>
|
||||||
[{status}] {title}
|
[{status}] {title}{' '}
|
||||||
|
<Chip
|
||||||
|
color={hasAcceptedAnswer ? 'success' : 'error'}
|
||||||
|
label={hasAcceptedAnswer ? 'Accepted' : 'Unaccepted'}
|
||||||
|
size='small'
|
||||||
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography noWrap variant='body1'>
|
<Typography noWrap variant='body1'>
|
||||||
{text.replace(/<[^>]*>?/gm, '')}
|
{text.replace(/<[^>]*>?/gm, '')}
|
||||||
</Typography>
|
</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>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
import { Typography } from "@mui/material"
|
import { Chip, Typography } from '@mui/material';
|
||||||
export default function ListQuestionInfo({upvotes, downvotes, answers, views, inline}){
|
export default function ListQuestionInfo({
|
||||||
return inline? (
|
answers,
|
||||||
|
downvotes,
|
||||||
|
inline,
|
||||||
|
hasAcceptedAnswer,
|
||||||
|
tags,
|
||||||
|
upvotes,
|
||||||
|
views,
|
||||||
|
}) {
|
||||||
|
return inline ? (
|
||||||
<>
|
<>
|
||||||
<Typography display = 'inline' variant='body1'>{upvotes - downvotes} votes | </Typography>
|
<Typography display='inline'>{upvotes - downvotes} votes | </Typography>
|
||||||
<Typography display = 'inline' variant='body1'>{answers} answers | </Typography>
|
<Typography display='inline'>{answers} answers | </Typography>
|
||||||
<Typography display = 'inline' variant='body1'>{views} views</Typography>
|
<Typography display='inline'>{views} views</Typography>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Typography display = 'block' variant='body1'>{upvotes - downvotes} votes</Typography>
|
<Typography display='block'>{upvotes - downvotes} votes</Typography>
|
||||||
<Typography display = 'block' variant='body1'>{answers} answers</Typography>
|
<Typography display='block'>{answers} answers</Typography>
|
||||||
<Typography display = 'block' variant='body1'>{views} views</Typography>
|
<Typography display='block'>{views} views</Typography>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,23 @@ import { Accordion, AccordionDetails, AccordionSummary, Box, Typography } from '
|
||||||
import ReactTimeAgo from 'react-time-ago';
|
import ReactTimeAgo from 'react-time-ago';
|
||||||
|
|
||||||
import { Markdown } from 'components';
|
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 (
|
return (
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
<AccordionSummary
|
||||||
|
expandIcon={<ExpandMoreIcon />}
|
||||||
|
onClick={() => { readMail(mail_id) }}
|
||||||
|
sx={read && { backgroundColor: 'lightGray' }}
|
||||||
|
>
|
||||||
<Box style={{ width: '100%' }} display={'flex'}>
|
<Box style={{ width: '100%' }} display={'flex'}>
|
||||||
<Typography>From {sender}</Typography>
|
<Typography>From {sender}</Typography>
|
||||||
<span style={{ width: '1vw' }} />
|
<span style={{ width: '1vw' }} />
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,16 @@ export default function MobileNavbar({ logout, userData, open, setOpen, mode, se
|
||||||
<ListItemText>Dashboard</ListItemText>
|
<ListItemText>Dashboard</ListItemText>
|
||||||
</ListItemButton>
|
</ListItemButton>
|
||||||
</ListItem>
|
</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>
|
<ListItem>
|
||||||
<ListItemButton color='inherit' component={Link} to='/mail'>
|
<ListItemButton color='inherit' component={Link} to='/mail'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default function PaginatedList({ count, Component, data, handleChangePage
|
||||||
<Component {...item} key={index} />
|
<Component {...item} key={index} />
|
||||||
))}
|
))}
|
||||||
{count > 1 && (
|
{count > 1 && (
|
||||||
<Box fullWidth display='flex' justifyContent='center' sx={{ margin: '1vh 0' }}>
|
<Box display='flex' justifyContent='center' sx={{ margin: '1vh 0' }}>
|
||||||
<Pagination
|
<Pagination
|
||||||
count={count}
|
count={count}
|
||||||
onChange={handleChangePage}
|
onChange={handleChangePage}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@ import { IconButton, Typography } from '@mui/material';
|
||||||
import Gravatar from 'react-gravatar';
|
import Gravatar from 'react-gravatar';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export default function Profile({ userData: { email, level, points } }) {
|
export default function Profile({ userData: { badgeCount, email, level, points } }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IconButton component={Link} to='/dashboard'>
|
<IconButton component={Link} to='/dashboard'>
|
||||||
<Gravatar email={email} size={20} style={{ borderRadius: '100%' }} />
|
<Gravatar email={email} size={20} style={{ borderRadius: '100%' }} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography variant='body1'>
|
<Typography display='inline'><b>{level}</b></Typography>
|
||||||
<b>{level}</b> - {points}
|
<Typography display='inline' m={1}>{points}</Typography>
|
||||||
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import ShareIcon from '@mui/icons-material/Share';
|
||||||
import { Button, ButtonGroup, ListItem, ListItemText, Tooltip } from '@mui/material';
|
import { Button, ButtonGroup, ListItem, ListItemText, Tooltip } from '@mui/material';
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
|
|
||||||
import { Markdown } from 'components';
|
import { Markdown } from 'components';
|
||||||
import { CreationInfoTag } from 'controllers';
|
import { CreationInfoTag } from 'controllers';
|
||||||
import { AnswerCommentsList, CommentControl, VoteControl } from 'controllers/QAControllers';
|
import { AnswerCommentsList, CommentControl, VoteControl } from 'controllers/QAControllers';
|
||||||
|
|
||||||
export default function Answer({
|
export default function Answer({
|
||||||
accepted,
|
|
||||||
answer_id,
|
answer_id,
|
||||||
|
question_id,
|
||||||
|
accepted,
|
||||||
comments,
|
comments,
|
||||||
creator,
|
creator,
|
||||||
createdAt,
|
createdAt,
|
||||||
downvotes,
|
downvotes,
|
||||||
text,
|
text,
|
||||||
upvotes,
|
upvotes,
|
||||||
|
acceptAnswer,
|
||||||
canComment,
|
canComment,
|
||||||
canAccept,
|
canAccept,
|
||||||
|
postComment,
|
||||||
getVote,
|
getVote,
|
||||||
updateVote,
|
updateVote,
|
||||||
postComment,
|
|
||||||
acceptAnswer,
|
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<span key={answer_id}>
|
<span key={answer_id}>
|
||||||
|
|
@ -47,12 +50,26 @@ export default function Answer({
|
||||||
<CreationInfoTag {...{ createdAt, creator, text: 'answered' }} />
|
<CreationInfoTag {...{ createdAt, creator, text: 'answered' }} />
|
||||||
<Markdown content={text} />
|
<Markdown content={text} />
|
||||||
<CommentControl {...{ canComment, postComment }} />
|
<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 && (
|
{canAccept && (
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
|
color='inherit'
|
||||||
|
size='small'
|
||||||
onClick={acceptAnswer}
|
onClick={acceptAnswer}
|
||||||
style={{ marginLeft: '10px' }}
|
style={{ textTransform: 'none' }}
|
||||||
variant='standard'
|
variant='text'
|
||||||
>
|
>
|
||||||
Accept
|
Accept
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -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 { CreationInfoTag } from 'controllers';
|
||||||
import { VoteControl } from 'controllers/QAControllers';
|
import { VoteControl } from 'controllers/QAControllers';
|
||||||
|
|
@ -11,6 +12,7 @@ export default function AnswerComment({
|
||||||
text,
|
text,
|
||||||
upvotes,
|
upvotes,
|
||||||
getVote,
|
getVote,
|
||||||
|
onDelete,
|
||||||
updateVote,
|
updateVote,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -21,6 +23,9 @@ export default function AnswerComment({
|
||||||
{text}
|
{text}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
<VoteControl {...{ downvotes, getVote, updateVote, upvotes }} />
|
<VoteControl {...{ downvotes, getVote, updateVote, upvotes }} />
|
||||||
|
{onDelete && <IconButton onClick={onDelete}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -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 { CreationInfoTag } from 'controllers';
|
||||||
import { VoteControl } from 'controllers/QAControllers';
|
import { VoteControl } from 'controllers/QAControllers';
|
||||||
|
|
@ -11,7 +12,8 @@ export default function Comment({
|
||||||
text,
|
text,
|
||||||
upvotes,
|
upvotes,
|
||||||
getVote,
|
getVote,
|
||||||
updateVote,
|
onDelete,
|
||||||
|
updateVote
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<span key={comment_id}>
|
<span key={comment_id}>
|
||||||
|
|
@ -21,6 +23,9 @@ export default function Comment({
|
||||||
{text}
|
{text}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
<VoteControl {...{ downvotes, getVote, updateVote, upvotes, creator }} />
|
<VoteControl {...{ downvotes, getVote, updateVote, upvotes, creator }} />
|
||||||
|
{onDelete && <IconButton onClick={onDelete}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<Divider />
|
<Divider />
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,12 @@ export default function CommentControl({
|
||||||
color='inherit'
|
color='inherit'
|
||||||
disableRipple
|
disableRipple
|
||||||
onClick={toggleShow}
|
onClick={toggleShow}
|
||||||
|
m={1}
|
||||||
size='small'
|
size='small'
|
||||||
startIcon={<AddCommentOutlinedIcon />}
|
startIcon={<AddCommentOutlinedIcon />}
|
||||||
style={{ textTransform: 'none' }}
|
style={{ textTransform: 'none' }}
|
||||||
variant='text'
|
variant='text'
|
||||||
>
|
>
|
||||||
<Typography>Comment</Typography>
|
<Typography>Comment</Typography>
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { AnswerForm } from 'controllers/FormControllers';
|
||||||
|
|
||||||
export default function CreateAnswer({ canAnswer, show, toggleShow }) {
|
export default function CreateAnswer({ canAnswer, show, toggleShow }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style = {{display : 'inline'}}>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
canAnswer
|
canAnswer
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,12 @@ import {
|
||||||
ListItemText,
|
ListItemText,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
|
TextField
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
|
import ShareIcon from '@mui/icons-material/Share';
|
||||||
|
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useState } from 'react';
|
||||||
import { Markdown } from 'components';
|
import { Markdown } from 'components';
|
||||||
import { CreationInfoTag } from 'controllers';
|
import { CreationInfoTag } from 'controllers';
|
||||||
import { CommentControl, VoteControl } from 'controllers/QAControllers';
|
import { CommentControl, VoteControl } from 'controllers/QAControllers';
|
||||||
|
|
@ -28,6 +31,7 @@ const statusColor = (status) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Question({
|
export default function Question({
|
||||||
|
question_id,
|
||||||
answers,
|
answers,
|
||||||
comments,
|
comments,
|
||||||
creator,
|
creator,
|
||||||
|
|
@ -35,6 +39,7 @@ export default function Question({
|
||||||
downvotes,
|
downvotes,
|
||||||
hasAcceptedAnswer,
|
hasAcceptedAnswer,
|
||||||
status,
|
status,
|
||||||
|
tags,
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
upvotes,
|
upvotes,
|
||||||
|
|
@ -48,8 +53,21 @@ export default function Question({
|
||||||
getVote,
|
getVote,
|
||||||
updateVote,
|
updateVote,
|
||||||
postComment,
|
postComment,
|
||||||
|
canBounty,
|
||||||
|
handleBounty,
|
||||||
|
hasBounty,
|
||||||
|
show,
|
||||||
}) {
|
}) {
|
||||||
|
const min = 75;
|
||||||
|
const max = 500;
|
||||||
|
const [value, setValue] = useState(75);
|
||||||
|
canBounty = canBounty && !hasBounty
|
||||||
|
|
||||||
|
function handleBountyFix() {
|
||||||
|
handleBounty(value)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Box m={2}>
|
<Box m={2}>
|
||||||
<Typography variant='h4'>{title}</Typography>
|
<Typography variant='h4'>{title}</Typography>
|
||||||
|
|
@ -75,10 +93,12 @@ export default function Question({
|
||||||
label={hasAcceptedAnswer ? 'yes' : 'no'}
|
label={hasAcceptedAnswer ? 'yes' : 'no'}
|
||||||
size='small'
|
size='small'
|
||||||
/>
|
/>
|
||||||
|
<Typography display='inline' m={1}>Tags: {tags.join(', ')}</Typography>
|
||||||
|
<br />
|
||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
to='../ask'
|
to='../ask'
|
||||||
style={{ marginLeft: '10px' }}
|
style={{ margin: '5px' }}
|
||||||
display='inline'
|
display='inline'
|
||||||
m={1}
|
m={1}
|
||||||
variant='contained'
|
variant='contained'
|
||||||
|
|
@ -90,9 +110,8 @@ export default function Question({
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
disabled={!canClose}
|
disabled={!canClose}
|
||||||
style={{ marginLeft: '10px' }}
|
style={{ margin: '5px' }}
|
||||||
display='inline'
|
display='inline'
|
||||||
m={1}
|
|
||||||
onClick={changeClose}
|
onClick={changeClose}
|
||||||
variant='contained'
|
variant='contained'
|
||||||
>
|
>
|
||||||
|
|
@ -107,7 +126,7 @@ export default function Question({
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
disabled={!canProtect}
|
disabled={!canProtect}
|
||||||
style={{ marginLeft: '10px' }}
|
style={{ margin: '5px' }}
|
||||||
display='inline'
|
display='inline'
|
||||||
m={1}
|
m={1}
|
||||||
variant='contained'
|
variant='contained'
|
||||||
|
|
@ -118,11 +137,54 @@ export default function Question({
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</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 && (
|
{ongoingVote.users.length > 0 && (
|
||||||
<Typography>
|
<Typography>
|
||||||
{ongoingVote.users.toString()} - voting to {ongoingVote.type} this question{' '}
|
{ongoingVote.users.toString()} - voting to {ongoingVote.type} this question{' '}
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
{hasBounty && (
|
||||||
|
<Typography>
|
||||||
|
there is a {hasBounty} point bounty on this question
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
|
|
@ -141,6 +203,18 @@ export default function Question({
|
||||||
<CreationInfoTag {...{ createdAt, creator }} />
|
<CreationInfoTag {...{ createdAt, creator }} />
|
||||||
<Markdown content={text} />
|
<Markdown content={text} />
|
||||||
<CommentControl {...{ postComment, canComment }} />
|
<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>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
68
client/src/components/QAComponents/Suggest.jsx
Normal file
68
client/src/components/QAComponents/Suggest.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Badges from './Badges';
|
||||||
import CreationInfoTag from './CreationInfoTag';
|
import CreationInfoTag from './CreationInfoTag';
|
||||||
import Form from './Form';
|
import Form from './Form';
|
||||||
import ListAnswer from './ListAnswer';
|
import ListAnswer from './ListAnswer';
|
||||||
|
|
@ -14,6 +15,7 @@ import Profile from './Profile';
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchBar';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
Badges,
|
||||||
CreationInfoTag,
|
CreationInfoTag,
|
||||||
Form,
|
Form,
|
||||||
ListAnswer,
|
ListAnswer,
|
||||||
|
|
|
||||||
|
|
@ -6,30 +6,23 @@ import { ListQuestion, LoadingBar } from 'components';
|
||||||
import { PaginatedList } from 'controllers';
|
import { PaginatedList } from 'controllers';
|
||||||
import { searchQuestions } from 'services/questionsServices';
|
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() {
|
export default function Buffet() {
|
||||||
const [sort, setSort] = useState(0);
|
const [sort, setSort] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [, setSearchParams] = useSearchParams();
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
const getData = async ({ question_id }) => {
|
const getData = async ({ question_id }) => {
|
||||||
const { questions } = await searchQuestions({
|
const { questions } = await searchQuestions({
|
||||||
...sortObjArr[sort],
|
sort,
|
||||||
after: question_id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setLoading(() => false);
|
setLoading(() => false);
|
||||||
return questions;
|
return questions;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSortChange = (_, newSort) => {
|
const handleSortChange = (_, value) => {
|
||||||
setSearchParams(sortObjArr[newSort]);
|
setSearchParams({ sort: value });
|
||||||
setSort(newSort);
|
setSort(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -60,10 +53,10 @@ export default function Buffet() {
|
||||||
onChange={handleSortChange}
|
onChange={handleSortChange}
|
||||||
style={{ display: 'block', marginTop: '1%' }}
|
style={{ display: 'block', marginTop: '1%' }}
|
||||||
>
|
>
|
||||||
<ToggleButton value={0}>Recent</ToggleButton>
|
<ToggleButton value=''>Recent</ToggleButton>
|
||||||
<ToggleButton value={1}>Best</ToggleButton>
|
<ToggleButton value='u'>Best</ToggleButton>
|
||||||
<ToggleButton value={2}>Interesting</ToggleButton>
|
<ToggleButton value='uvc'>Interesting</ToggleButton>
|
||||||
<ToggleButton value={3}>Hot</ToggleButton>
|
<ToggleButton value='uvac'>Hot</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
</Box>
|
</Box>
|
||||||
{loading && <LoadingBar />}
|
{loading && <LoadingBar />}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { ListAnswer, ListQuestion, LoadingBar } from 'components';
|
import { ListAnswer, ListQuestion, LoadingBar } from 'components';
|
||||||
import { useUser } from 'contexts';
|
import { useUser } from 'contexts';
|
||||||
import { PaginatedList } from 'controllers';
|
import { Badges, PaginatedList } from 'controllers';
|
||||||
import { deleteUser, getUserAnswers, getUserQuestions } from 'services/userServices';
|
import { deleteUser, getUserAnswers, getUserQuestions } from 'services/userServices';
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
|
|
@ -25,7 +25,7 @@ export default function Dashboard() {
|
||||||
const [current, setCurrent] = useState('questions');
|
const [current, setCurrent] = useState('questions');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false)
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userData: { email, level, points, username },
|
userData: { email, level, points, username },
|
||||||
|
|
@ -62,16 +62,16 @@ export default function Dashboard() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCurrentUser = () => {
|
const deleteCurrentUser = () => {
|
||||||
if(confirmDelete){
|
if (confirmDelete) {
|
||||||
deleteUser();
|
deleteUser();
|
||||||
navigate('/users/login')
|
navigate('/users/login')
|
||||||
setConfirmDelete(false);
|
setConfirmDelete(false);
|
||||||
}else{
|
} else {
|
||||||
setConfirmDelete(true);
|
setConfirmDelete(true);
|
||||||
setTimeout(cancelDelete, 30000)
|
setTimeout(cancelDelete, 30000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function cancelDelete(){
|
function cancelDelete() {
|
||||||
setConfirmDelete(false)
|
setConfirmDelete(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,18 +89,21 @@ export default function Dashboard() {
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
spacing={2}
|
spacing={2}
|
||||||
sx={{
|
// sx={{
|
||||||
display: { xs: 'flex', sm: 'block', md: 'block' },
|
// display: { xs: 'flex', sm: 'block', md: 'block' },
|
||||||
justifyContent: 'center',
|
// justifyContent: 'center',
|
||||||
}}
|
// }}
|
||||||
>
|
>
|
||||||
<Grid item sm={4} md={2}>
|
<Grid item sm={4} md={3}>
|
||||||
{email && (
|
{email && (
|
||||||
<Gravatar email={email} size={200} style={{ borderRadius: '100%' }} />
|
<Gravatar email={email} size={200} style={{ borderRadius: '100%' }} />
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item sm={12} md={8}>
|
||||||
|
<Badges />
|
||||||
|
</Grid>
|
||||||
<Grid item sm={8} md={10}>
|
<Grid item sm={8} md={10}>
|
||||||
{confirmDelete && <Alert variant = "warning">Click delete button again to confirm delete, cancelling in 30 seconds</Alert>}
|
{confirmDelete && <Alert variant="warning">Click delete button again to confirm delete, cancelling in 30 seconds</Alert>}
|
||||||
<Typography>Username: {username}</Typography>
|
<Typography>Username: {username}</Typography>
|
||||||
<Typography>Email: {email}</Typography>
|
<Typography>Email: {email}</Typography>
|
||||||
<Typography>Level: {level}</Typography>
|
<Typography>Level: {level}</Typography>
|
||||||
|
|
@ -127,7 +130,7 @@ export default function Dashboard() {
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color='warning'
|
color='warning'
|
||||||
onClick = {deleteCurrentUser}
|
onClick={deleteCurrentUser}
|
||||||
sx={{ margin: '0 1vh' }}
|
sx={{ margin: '0 1vh' }}
|
||||||
variant='outlined'
|
variant='outlined'
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { List, ListItem } from '@mui/material';
|
import { List, ListItem } from '@mui/material';
|
||||||
|
import Suggest from 'components/QAComponents/Suggest';
|
||||||
import { AnswersList, CommentsList, CreateAnswer, Question } from 'controllers/QAControllers';
|
import { AnswersList, CommentsList, CreateAnswer, Question } from 'controllers/QAControllers';
|
||||||
|
|
||||||
export default function QA() {
|
export default function QA() {
|
||||||
|
|
@ -8,7 +9,9 @@ export default function QA() {
|
||||||
<ListItem sx={{ pl: 4 }}>
|
<ListItem sx={{ pl: 4 }}>
|
||||||
<CommentsList />
|
<CommentsList />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<CreateAnswer />
|
<CreateAnswer/>
|
||||||
|
<Suggest/>
|
||||||
|
<div id='answersList' />
|
||||||
<AnswersList />
|
<AnswersList />
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,6 @@ export default function UserProvider({ children }) {
|
||||||
const [userData, setUserData] = useState(initialUserData)
|
const [userData, setUserData] = useState(initialUserData)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadUserData = async () => {
|
const loadUserData = async () => {
|
||||||
const { user, error } = await remember();
|
const { user, error } = await remember();
|
||||||
|
|
|
||||||
20
client/src/controllers/BadgesController.js
Normal file
20
client/src/controllers/BadgesController.js
Normal 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 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { Form } from 'components';
|
import { Form } from 'components';
|
||||||
import { useForm } from 'contexts';
|
import { useForm } from 'contexts';
|
||||||
|
|
||||||
|
|
@ -10,11 +9,15 @@ export default function FormController({
|
||||||
validate,
|
validate,
|
||||||
validationSchema,
|
validationSchema,
|
||||||
children,
|
children,
|
||||||
|
initialValues = {}
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
|
|
||||||
const { setContent } = useForm();
|
const { setContent } = useForm();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setContent('');
|
setContent('');
|
||||||
|
|
||||||
}, [setContent]);
|
}, [setContent]);
|
||||||
|
|
||||||
const handleChange = (e) => {
|
const handleChange = (e) => {
|
||||||
|
|
@ -22,7 +25,7 @@ export default function FormController({
|
||||||
};
|
};
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: fields.reduce((acc, { id }) => ({ ...acc, [id]: '' }), {}),
|
initialValues: fields.reduce((acc, { id }) => ({ ...acc, [id]: initialValues[id] || '' }), {}),
|
||||||
onSubmit,
|
onSubmit,
|
||||||
validate,
|
validate,
|
||||||
validateOnChange: false,
|
validateOnChange: false,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ export default function PaginatedListController({
|
||||||
count,
|
count,
|
||||||
Component,
|
Component,
|
||||||
getData,
|
getData,
|
||||||
|
scroll,
|
||||||
rowsPerPage = 5,
|
rowsPerPage = 5,
|
||||||
}) {
|
}) {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
@ -25,7 +26,19 @@ export default function PaginatedListController({
|
||||||
const newData = await getData(clear ? {} : data[data.length - 1] ?? {});
|
const newData = await getData(clear ? {} : data[data.length - 1] ?? {});
|
||||||
|
|
||||||
if (newData?.length)
|
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 setData(data.concat(newData));
|
||||||
else setLoad(false);
|
else setLoad(false);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,19 @@
|
||||||
|
import { useUser } from 'contexts';
|
||||||
import { AnswerComment } from 'components/QAComponents';
|
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 }) {
|
export default function AnswerCommentController({ answer_id, comment_id, question_id, ...props }) {
|
||||||
|
const { userData } = useUser();
|
||||||
|
|
||||||
const getVote = () => getAnswerCommentVote(question_id, answer_id, comment_id);
|
const getVote = () => getAnswerCommentVote(question_id, answer_id, comment_id);
|
||||||
const updateVote = (data) => updateAnswerCommentVote(question_id, answer_id, comment_id, data);
|
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 }} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,10 @@ import {
|
||||||
} from 'services/questionsServices';
|
} from 'services/questionsServices';
|
||||||
|
|
||||||
export default function AnswerController({ answer_id, question_id, ...props }) {
|
export default function AnswerController({ answer_id, question_id, ...props }) {
|
||||||
|
const acceptAnswer = () => updateAcceptAnswer(question_id, answer_id);
|
||||||
const getVote = () => getAnswerVote(question_id, answer_id);
|
const getVote = () => getAnswerVote(question_id, answer_id);
|
||||||
const updateVote = (data) => updateAnswerVote(question_id, answer_id, data);
|
const updateVote = (data) => updateAnswerVote(question_id, answer_id, data);
|
||||||
const postComment = async (data) => {
|
const postComment = async (data) => { await postAnswerComment(question_id, answer_id, data); };
|
||||||
await postAnswerComment(question_id, answer_id, data);
|
|
||||||
};
|
|
||||||
const acceptAnswer = () => updateAcceptAnswer(question_id, answer_id);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Answer
|
<Answer
|
||||||
|
|
@ -22,8 +20,8 @@ export default function AnswerController({ answer_id, question_id, ...props }) {
|
||||||
updateVote,
|
updateVote,
|
||||||
postComment,
|
postComment,
|
||||||
acceptAnswer,
|
acceptAnswer,
|
||||||
question_id,
|
|
||||||
answer_id,
|
answer_id,
|
||||||
|
question_id,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,20 @@
|
||||||
|
import { useUser } from 'contexts';
|
||||||
import { Comment } from 'components/QAComponents';
|
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 }) {
|
export default function CommentController({ comment_id, question_id, ...props }) {
|
||||||
|
const { userData } = useUser();
|
||||||
|
|
||||||
const getVote = () => getCommentVote(question_id, comment_id);
|
const getVote = () => getCommentVote(question_id, comment_id);
|
||||||
const updateVote = (data) => updateCommentVote(question_id, comment_id, data);
|
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 }} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,16 +27,33 @@ export function AnswersList() {
|
||||||
const sortByPoints = (answers) =>
|
const sortByPoints = (answers) =>
|
||||||
answers.sort((a, b) => b.upvotes - b.downvotes - a.upvotes + a.downvotes);
|
answers.sort((a, b) => b.upvotes - b.downvotes - a.upvotes + a.downvotes);
|
||||||
|
|
||||||
const getData = ({ answer_id }) =>
|
const getData = async ({ answer_id }) => {
|
||||||
getAnswers(question_id, { after: answer_id })
|
const answersList = await getAnswers(question_id, { after: answer_id })
|
||||||
.then(({ answers }) =>
|
.then(({ answers }) =>
|
||||||
sortByPoints(answers).map((answer) => ({ ...answer, question_id }))
|
sortByPoints(answers).map((answer) => ({ ...answer, question_id }))
|
||||||
)
|
)
|
||||||
.catch(() => []);
|
.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 }} />;
|
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() {
|
export function CommentsList() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,9 @@ import { Question } from 'components/QAComponents';
|
||||||
import { useQuestion, useUser } from 'contexts';
|
import { useQuestion, useUser } from 'contexts';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import getPermissions from 'services/getPermissions';
|
import getPermissions from 'services/getPermissions';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
addBounty,
|
||||||
closeQuestion,
|
closeQuestion,
|
||||||
getQuestionVote,
|
getQuestionVote,
|
||||||
openQuestion,
|
openQuestion,
|
||||||
|
|
@ -13,17 +15,18 @@ import {
|
||||||
updateQuestion,
|
updateQuestion,
|
||||||
updateQuestionVote,
|
updateQuestionVote,
|
||||||
} from 'services/questionsServices';
|
} from 'services/questionsServices';
|
||||||
|
import { SettingsPhoneTwoTone } from '@mui/icons-material';
|
||||||
|
|
||||||
export default function QuestionController() {
|
export default function QuestionController() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
let canBounty = false;
|
||||||
const { question_id } = useParams();
|
const { question_id } = useParams();
|
||||||
const { questionData } = useQuestion();
|
const { questionData } = useQuestion();
|
||||||
const { protect, close, reopen, status } = questionData;
|
const { protect, close, reopen, status } = questionData;
|
||||||
const { userData } = useUser();
|
const { userData } = useUser();
|
||||||
const [ongoingVote, setOngoingVote] = useState({ users: [], type: 'none' });
|
const [ongoingVote, setOngoingVote] = useState({ users: [], type: 'none' });
|
||||||
const { permissions, setPermissions } = useQuestion();
|
const { permissions, setPermissions } = useQuestion();
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
function setVote() {
|
function setVote() {
|
||||||
if (!questionData.loading) {
|
if (!questionData.loading) {
|
||||||
if (protect.length) setOngoingVote({ users: protect, type: 'protect' });
|
if (protect.length) setOngoingVote({ users: protect, type: 'protect' });
|
||||||
|
|
@ -34,7 +37,9 @@ export default function QuestionController() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const permissions = getPermissions(questionData, userData, ongoingVote);
|
const permissions = getPermissions(questionData, userData, ongoingVote);
|
||||||
|
if (userData.username) canBounty = (userData.level >= 4 && status !== 'closed' && !questionData.hasBounty && !questionData.hasAcceptedAnswer)
|
||||||
|
|
||||||
|
permissions.canBounty = canBounty;
|
||||||
setPermissions(permissions);
|
setPermissions(permissions);
|
||||||
setVote();
|
setVote();
|
||||||
}, [userData, questionData]);
|
}, [userData, questionData]);
|
||||||
|
|
@ -55,6 +60,7 @@ export default function QuestionController() {
|
||||||
? protect.splice(protect.indexOf(userData.username), 1)
|
? protect.splice(protect.indexOf(userData.username), 1)
|
||||||
: protect.push(userData.username);
|
: protect.push(userData.username);
|
||||||
ongoingVoteSet(protect, 'protect');
|
ongoingVoteSet(protect, 'protect');
|
||||||
|
window.location.reload(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeClose() {
|
function changeClose() {
|
||||||
|
|
@ -64,12 +70,25 @@ export default function QuestionController() {
|
||||||
? close.splice(close.indexOf(userData.username), 1)
|
? close.splice(close.indexOf(userData.username), 1)
|
||||||
: close.push(userData.username);
|
: close.push(userData.username);
|
||||||
ongoingVoteSet(close, 'close');
|
ongoingVoteSet(close, 'close');
|
||||||
|
window.location.reload(false);
|
||||||
} else {
|
} else {
|
||||||
openQuestion(question_id, userData);
|
openQuestion(question_id, userData);
|
||||||
reopen.includes(userData.username)
|
reopen.includes(userData.username)
|
||||||
? reopen.splice(reopen.indexOf(userData.username), 1)
|
? reopen.splice(reopen.indexOf(userData.username), 1)
|
||||||
: reopen.push(userData.username);
|
: reopen.push(userData.username);
|
||||||
ongoingVoteSet(reopen, 'reopen');
|
ongoingVoteSet(reopen, 'reopen');
|
||||||
|
window.location.reload(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleBounty(amount) {
|
||||||
|
if (show) {
|
||||||
|
const data = {amount: amount}
|
||||||
|
addBounty(question_id, data)
|
||||||
|
questionData.hasBounty = amount;
|
||||||
|
userData.points -= amount;
|
||||||
|
setShow(false)
|
||||||
|
} else {
|
||||||
|
setShow(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,6 +112,9 @@ export default function QuestionController() {
|
||||||
getVote,
|
getVote,
|
||||||
updateVote,
|
updateVote,
|
||||||
postComment,
|
postComment,
|
||||||
|
show,
|
||||||
|
handleBounty
|
||||||
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ export default function SearchBarController() {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (location.pathname === '/questions/search') {
|
if (location.pathname === '/questions/search') {
|
||||||
setSearchParams({ title: search, text: search, creator: search }, { replace: true });
|
setSearchParams({ title: search }, { replace: true });
|
||||||
} else {
|
} else {
|
||||||
navigate({
|
navigate({
|
||||||
pathname: '/questions/search',
|
pathname: '/questions/search',
|
||||||
search: `?${createSearchParams({
|
search: `?${createSearchParams({
|
||||||
title: search, text: search, creator: search
|
title: search,
|
||||||
})}`,
|
})}`,
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,31 +9,26 @@ export default function SearchResultsController() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const getData = async ({ question_id }) => {
|
const getData = async () => {
|
||||||
let match = {};
|
const creator = searchParams.get('creator');
|
||||||
const regexMatch = {};
|
const title = searchParams.get('title');
|
||||||
const time = {};
|
const text = searchParams.get('text');
|
||||||
let newdate = searchParams.get('createdAt');
|
let tags = searchParams.get('tags');
|
||||||
|
let time = searchParams.get('createdAt');
|
||||||
|
let createdAt = {};
|
||||||
|
|
||||||
if (newdate) {
|
if (time) {
|
||||||
newdate = new Date(newdate);
|
time = new Date(time);
|
||||||
time['$gte'] = parseInt(newdate.getTime());
|
createdAt['$gte'] = parseInt(time.getTime());
|
||||||
newdate.setDate(newdate.getDate() + 1);
|
time.setDate(time.getDate() + 1);
|
||||||
time['$lte'] = newdate.getTime();
|
createdAt['$lte'] = time.getTime();
|
||||||
match = JSON.stringify({ createdAt: time });
|
}
|
||||||
|
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);
|
setLoading(() => true);
|
||||||
const { questions } = await searchQuestions({ after: question_id, regexMatch, match });
|
const { questions } = await searchQuestions({ creator, title, text, tags, createdAt });
|
||||||
setLoading(() => false);
|
setLoading(() => false);
|
||||||
|
|
||||||
return questions;
|
return questions;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Badges from './BadgesController';
|
||||||
import CreationInfoTag from './CreationInfoTagController';
|
import CreationInfoTag from './CreationInfoTagController';
|
||||||
import Inbox from './InboxController';
|
import Inbox from './InboxController';
|
||||||
import MdPreview from './MdPreviewController';
|
import MdPreview from './MdPreviewController';
|
||||||
|
|
@ -6,4 +7,4 @@ import PaginatedList from './PaginatedListController';
|
||||||
import SearchBar from './SearchBarController';
|
import SearchBar from './SearchBarController';
|
||||||
import SearchResults from './SearchResultsController';
|
import SearchResults from './SearchResultsController';
|
||||||
|
|
||||||
export { CreationInfoTag, Inbox, MdPreview, Navbar, PaginatedList, SearchBar, SearchResults };
|
export { Badges, CreationInfoTag, Inbox, MdPreview, Navbar, PaginatedList, SearchBar, SearchResults };
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const askQuestionFields = [
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
multiline: true,
|
multiline: true,
|
||||||
},
|
},
|
||||||
|
{ id: 'tags', label: 'Tags' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const commentFields = [
|
const commentFields = [
|
||||||
|
|
@ -127,8 +128,25 @@ const searchFields = [
|
||||||
id: 'creator',
|
id: 'creator',
|
||||||
label: 'Creator',
|
label: 'Creator',
|
||||||
},
|
},
|
||||||
|
{ id: 'tags', label: 'Tags (separated by spaces)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const generateEditFields =
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'etitle',
|
||||||
|
label: 'Title',
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'etext',
|
||||||
|
label: 'Text',
|
||||||
|
multiline: true,
|
||||||
|
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export {
|
export {
|
||||||
answerFields,
|
answerFields,
|
||||||
askQuestionFields,
|
askQuestionFields,
|
||||||
|
|
@ -140,4 +158,5 @@ export {
|
||||||
recoverFields,
|
recoverFields,
|
||||||
resetFields,
|
resetFields,
|
||||||
searchFields,
|
searchFields,
|
||||||
|
generateEditFields
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
|
||||||
function getPermissions(questionData, userData, ongoingVote) {
|
function getPermissions(questionData, userData, ongoingVote) {
|
||||||
const { creator, status, hasAcceptedAnswer } = questionData;
|
|
||||||
|
const { creator, status, hasAcceptedAnswer, close, reopen, protect } = questionData;
|
||||||
const { username } = userData;
|
const { username } = userData;
|
||||||
|
|
||||||
var permissions = {
|
var permissions = {
|
||||||
|
|
@ -10,19 +12,24 @@ function getPermissions(questionData, userData, ongoingVote) {
|
||||||
canAccept: false,
|
canAccept: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const level = userData.level || 0;
|
const level = userData.level || 0;
|
||||||
const protection = status === 'protected' || status === 'closed';
|
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) {
|
if (userData.username === creator && !hasAcceptedAnswer) {
|
||||||
permissions.canAccept = true;
|
permissions.canAccept = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level >= 7 && ongoingVote.type !== 'protect') {
|
if (level >= 7 && ongoingVote.type !== 'protect' && !ongoingProtect) {
|
||||||
permissions.canClose = true;
|
permissions.canClose = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level >= 6 && ongoingVote.type !== 'close' && !protection) {
|
if (level >= 6 && ongoingVote.type !== 'close' && !protection && !ongoingClose) {
|
||||||
permissions.canProtect = true;
|
permissions.canProtect = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
|
"Level 1": 1,
|
||||||
"Level 2": 15,
|
"Level 2": 15,
|
||||||
"Level 3": 50,
|
"Level 3": 50,
|
||||||
"Level 4": 125,
|
"Level 4": 75,
|
||||||
"Level 5": 1000,
|
"Level 5": 125,
|
||||||
"Level 6": 3000,
|
"Level 6": 1000,
|
||||||
"Level 7": 10000
|
"Level 7": 2000,
|
||||||
|
"Level 8": 3000,
|
||||||
|
"Level 9": 10000
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ import { createEndpoint } from './api';
|
||||||
|
|
||||||
const callMailAPI = createEndpoint('/mail');
|
const callMailAPI = createEndpoint('/mail');
|
||||||
|
|
||||||
const postMail = async (data) => callMailAPI('post', ``, data);
|
|
||||||
|
|
||||||
const getMail = async () => callMailAPI('get', ``);
|
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 };
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,18 @@ const postQuestion = async (data) => callQuestionsAPI('post', ``, data);
|
||||||
|
|
||||||
const getQuestion = async (question_id) => callQuestionsAPI('get', `/${question_id}`);
|
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) =>
|
const updateQuestion = async (question_id, data) =>
|
||||||
callQuestionsAPI('patch', `/${question_id}`, data);
|
callQuestionsAPI('patch', `/${question_id}`, data);
|
||||||
|
|
||||||
|
|
@ -103,4 +115,6 @@ export {
|
||||||
closeQuestion,
|
closeQuestion,
|
||||||
openQuestion,
|
openQuestion,
|
||||||
protectQuestion,
|
protectQuestion,
|
||||||
|
addBounty,
|
||||||
|
editQuestion
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 '),
|
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'),
|
// 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'),
|
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({
|
const loginSchema = Yup.object({
|
||||||
|
|
@ -71,6 +75,10 @@ const questionSchema = Yup.object({
|
||||||
text: Yup.string()
|
text: Yup.string()
|
||||||
.max(3000, 'Body cannot be longer than 3000 characters')
|
.max(3000, 'Body cannot be longer than 3000 characters')
|
||||||
.required('A body is required'),
|
.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(
|
const patchSchema = Yup.object().shape(
|
||||||
|
|
@ -99,6 +107,14 @@ const patchSchema = Yup.object().shape(
|
||||||
['password', 'password'],
|
['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 {
|
export {
|
||||||
answerSchema,
|
answerSchema,
|
||||||
|
|
@ -111,4 +127,5 @@ export {
|
||||||
registerSchema,
|
registerSchema,
|
||||||
resetSchema,
|
resetSchema,
|
||||||
searchSchema,
|
searchSchema,
|
||||||
|
editSchema
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,47 +6,49 @@ import { createEndpoint } from './api';
|
||||||
const callUsersAPI = createEndpoint('/users');
|
const callUsersAPI = createEndpoint('/users');
|
||||||
|
|
||||||
const getLoginAttempts = () => {
|
const getLoginAttempts = () => {
|
||||||
let loginAttempts = Number(Cookies.get('loginAttempts')) || 0;
|
let loginAttempts = Number(Cookies.get('loginAttempts')) || 0;
|
||||||
let loginTimeout = Number(Cookies.get('loginTimeout')) - Date.now() || 0;
|
let loginTimeout = Number(Cookies.get('loginTimeout')) - Date.now() || 0;
|
||||||
|
|
||||||
if (loginTimeout < 0) {
|
if (loginTimeout < 0) {
|
||||||
Cookies.remove('loginAttempts');
|
Cookies.remove('loginAttempts');
|
||||||
Cookies.remove('loginTimeout');
|
Cookies.remove('loginTimeout');
|
||||||
loginAttempts = 0;
|
loginAttempts = 0;
|
||||||
loginTimeout = 0;
|
loginTimeout = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { loginAttempts, loginTimeout };
|
return { loginAttempts, loginTimeout };
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUser = async (username) => callUsersAPI('get', `/${username}`);
|
const getUser = async (username) => callUsersAPI('get', `/${username}`);
|
||||||
|
|
||||||
const getUserQuestions = async () => callUsersAPI('get', `/questions`);
|
|
||||||
|
|
||||||
const getUserAnswers = async () => callUsersAPI('get', `/answers`);
|
const getUserAnswers = async () => callUsersAPI('get', `/answers`);
|
||||||
|
|
||||||
const deleteUser = async () => callUsersAPI('delete',`/delete`)
|
const getUserBadges = async () => callUsersAPI('get', `/badges`);
|
||||||
|
|
||||||
|
const getUserQuestions = async () => callUsersAPI('get', `/questions`);
|
||||||
|
|
||||||
|
const deleteUser = async () => callUsersAPI('delete', `/delete`)
|
||||||
|
|
||||||
const incrementLoginAttempts = () => {
|
const incrementLoginAttempts = () => {
|
||||||
const { loginAttempts } = getLoginAttempts();
|
const { loginAttempts } = getLoginAttempts();
|
||||||
Cookies.set('loginAttempts', loginAttempts + 1);
|
Cookies.set('loginAttempts', loginAttempts + 1);
|
||||||
if (loginAttempts >= 2) Cookies.set('loginTimeout', Date.now() + 1000 * 60 * 5);
|
if (loginAttempts >= 2) Cookies.set('loginTimeout', Date.now() + 1000 * 60 * 5);
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async ({ username, password }) => {
|
const login = async ({ username, password }) => {
|
||||||
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
const encoded = Buffer.from(`${username}:${password}`).toString('base64');
|
||||||
const { token, ...data } = await callUsersAPI(
|
const { token, ...data } = await callUsersAPI(
|
||||||
'post',
|
'post',
|
||||||
`/login`,
|
`/login`,
|
||||||
{ remember: false },
|
{ remember: false },
|
||||||
`basic ${encoded}`
|
`basic ${encoded}`
|
||||||
);
|
);
|
||||||
if (token) {
|
if (token) {
|
||||||
Cookies.set('token', token);
|
Cookies.set('token', token);
|
||||||
Cookies.remove('loginAttempts');
|
Cookies.remove('loginAttempts');
|
||||||
Cookies.remove('loginTimeout');
|
Cookies.remove('loginTimeout');
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = async () => callUsersAPI('post', `/login`, { remember: false });
|
const logout = async () => callUsersAPI('post', `/login`, { remember: false });
|
||||||
|
|
@ -56,13 +58,13 @@ const updateUser = async (data) => callUsersAPI('patch', ``, data);
|
||||||
const register = async (data) => callUsersAPI('post', ``, data);
|
const register = async (data) => callUsersAPI('post', ``, data);
|
||||||
|
|
||||||
const remember = async () => {
|
const remember = async () => {
|
||||||
if (Cookies.get('token')) {
|
if (Cookies.get('token')) {
|
||||||
const { user, error } = await callUsersAPI('get', `/remember`);
|
const { user, error } = await callUsersAPI('get', `/remember`);
|
||||||
if (user)
|
if (user)
|
||||||
return { user };
|
return { user };
|
||||||
Cookies.remove('token');
|
Cookies.remove('token');
|
||||||
return { error }
|
return { error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestReset = async (data) => callUsersAPI('post', `/reset`, data);
|
const requestReset = async (data) => callUsersAPI('post', `/reset`, data);
|
||||||
|
|
@ -70,17 +72,18 @@ const requestReset = async (data) => callUsersAPI('post', `/reset`, data);
|
||||||
const resetPassword = async (id, data) => callUsersAPI('post', `/reset/${id}`, data);
|
const resetPassword = async (id, data) => callUsersAPI('post', `/reset/${id}`, data);
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getLoginAttempts,
|
getLoginAttempts,
|
||||||
getUser,
|
getUser,
|
||||||
getUserAnswers,
|
getUserAnswers,
|
||||||
getUserQuestions,
|
getUserBadges,
|
||||||
incrementLoginAttempts,
|
getUserQuestions,
|
||||||
login,
|
incrementLoginAttempts,
|
||||||
logout,
|
login,
|
||||||
register,
|
logout,
|
||||||
remember,
|
register,
|
||||||
updateUser,
|
remember,
|
||||||
requestReset,
|
updateUser,
|
||||||
resetPassword,
|
requestReset,
|
||||||
deleteUser
|
resetPassword,
|
||||||
|
deleteUser
|
||||||
};
|
};
|
||||||
|
|
|
||||||
52
package-lock.json
generated
52
package-lock.json
generated
|
|
@ -12,6 +12,9 @@
|
||||||
"client",
|
"client",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"react-copy-to-clipboard": "^5.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^7.3.0"
|
"concurrently": "^7.3.0"
|
||||||
}
|
}
|
||||||
|
|
@ -32,6 +35,7 @@
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"marked": "^4.0.18",
|
"marked": "^4.0.18",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-gravatar": "^2.6.3",
|
"react-gravatar": "^2.6.3",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
|
|
@ -6566,6 +6570,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
|
||||||
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
|
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/copy-to-clipboard": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
|
||||||
|
"dependencies": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-js": {
|
"node_modules/core-js": {
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
||||||
|
|
@ -16545,6 +16557,18 @@
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-copy-to-clipboard": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||||
|
"dependencies": {
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^15.3.0 || 16 || 17 || 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dev-utils": {
|
"node_modules/react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
|
|
@ -18943,6 +18967,11 @@
|
||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
@ -25205,6 +25234,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.3.tgz",
|
||||||
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
|
"integrity": "sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ=="
|
||||||
},
|
},
|
||||||
|
"copy-to-clipboard": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-Vme1Z6RUDzrb6xAI7EZlVZ5uvOk2F//GaxKUxajDqm9LhOVM1inxNAD2vy+UZDYsd0uyA9s7b3/FVZPSxqrCfg==",
|
||||||
|
"requires": {
|
||||||
|
"toggle-selection": "^1.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "3.24.1",
|
"version": "3.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.24.1.tgz",
|
||||||
|
|
@ -32141,6 +32178,7 @@
|
||||||
"js-cookie": "^3.0.1",
|
"js-cookie": "^3.0.1",
|
||||||
"marked": "^4.0.18",
|
"marked": "^4.0.18",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-gravatar": "^2.6.3",
|
"react-gravatar": "^2.6.3",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-markdown": "^8.0.3",
|
"react-markdown": "^8.0.3",
|
||||||
|
|
@ -32233,6 +32271,15 @@
|
||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-copy-to-clipboard": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==",
|
||||||
|
"requires": {
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-dev-utils": {
|
"react-dev-utils": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
|
||||||
|
|
@ -34023,6 +34070,11 @@
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"toggle-selection": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
|
||||||
|
},
|
||||||
"toidentifier": {
|
"toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,8 @@
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^7.3.0"
|
"concurrently": "^7.3.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-copy-to-clipboard": "^5.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
64
server/badges.json
Normal file
64
server/badges.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -43,8 +43,8 @@ mongoose.connection.on(
|
||||||
console.error.bind(console, '[ERROR]: MongoDB connection error - ')
|
console.error.bind(console, '[ERROR]: MongoDB connection error - ')
|
||||||
);
|
);
|
||||||
|
|
||||||
//cron.schedule('*/30 * * * * *', refreshQuestions);
|
// cron.schedule('*/30 * * * * *', refreshQuestions);
|
||||||
//cron.schedule('*/2 * * * *', refreshUsers);
|
// cron.schedule('*/2 * * * *', refreshUsers);
|
||||||
|
|
||||||
server.listen(port, console.log(`Listening on port: ${port}`));
|
server.listen(port, console.log(`Listening on port: ${port}`));
|
||||||
server.on('error', onError);
|
server.on('error', onError);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
const calculateAnswerBadges = require('../../utils/badges/answerBadges');
|
||||||
|
const User = require('./User');
|
||||||
|
|
||||||
const Answer = mongoose.Schema(
|
const Answer = mongoose.Schema(
|
||||||
{
|
{
|
||||||
|
|
@ -15,4 +17,11 @@ const Answer = mongoose.Schema(
|
||||||
{ timestamps: { createdAt: false, updatedAt: true } }
|
{ 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);
|
module.exports = mongoose.model('Answer', Answer);
|
||||||
|
|
|
||||||
9
server/db/models/Badge.js
Normal file
9
server/db/models/Badge.js
Normal 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);
|
||||||
|
|
@ -8,6 +8,7 @@ const Mail = mongoose.Schema(
|
||||||
sender: { type: String, required: true },
|
sender: { type: String, required: true },
|
||||||
subject: { type: String, required: true },
|
subject: { type: String, required: true },
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
|
read: { type: Boolean, required: true, default: false },
|
||||||
},
|
},
|
||||||
{ timestamps: false }
|
{ timestamps: false }
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
const calculateQuestionBadges = require('../../utils/badges/questionBadges');
|
||||||
|
const User = require('./User');
|
||||||
|
|
||||||
const Question = mongoose.Schema(
|
const Question = mongoose.Schema(
|
||||||
{
|
{
|
||||||
answers: { type: Number, required: true, default: 0 },
|
answers: { type: Number, required: true, default: 0 },
|
||||||
close: [String],
|
|
||||||
comments: { type: Number, required: true, default: 0 },
|
comments: { type: Number, required: true, default: 0 },
|
||||||
createdAt: { type: Date, required: true, default: Date.now },
|
createdAt: { type: Date, required: true, default: Date.now },
|
||||||
creator: { type: String, required: true },
|
creator: { type: String, required: true },
|
||||||
|
|
@ -12,15 +13,27 @@ const Question = mongoose.Schema(
|
||||||
lastAnswerFetch: { type: Date, default: new Date(0) },
|
lastAnswerFetch: { type: Date, default: new Date(0) },
|
||||||
lastCommentFetch: { type: Date, default: new Date(0) },
|
lastCommentFetch: { type: Date, default: new Date(0) },
|
||||||
protect: [String],
|
protect: [String],
|
||||||
question_id: { type: String, required: true },
|
|
||||||
reopen: [String],
|
reopen: [String],
|
||||||
|
close: [String],
|
||||||
|
edit: [String],
|
||||||
|
editText: [String],
|
||||||
|
tags: [String],
|
||||||
|
question_id: { type: String, required: true },
|
||||||
status: { type: String, required: true, default: 'open' },
|
status: { type: String, required: true, default: 'open' },
|
||||||
text: { type: String, required: true },
|
text: { type: String, required: true },
|
||||||
title: { type: String, required: true },
|
title: { type: String, required: true },
|
||||||
upvotes: { type: Number, required: true, default: 0 },
|
upvotes: { type: Number, required: true, default: 0 },
|
||||||
views: { type: Number, required: true, default: 0 },
|
views: { type: Number, required: true, default: 0 },
|
||||||
|
hasBounty: { type: Number, required: false },
|
||||||
},
|
},
|
||||||
{ timestamps: { createdAt: false, updatedAt: true } }
|
{ 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);
|
module.exports = mongoose.model('Question', Question);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
const calculateUserBadges = require('server/utils/badges/userBadges');
|
||||||
|
|
||||||
const User = mongoose.Schema(
|
const User = mongoose.Schema(
|
||||||
{
|
{
|
||||||
|
|
@ -9,8 +10,15 @@ const User = mongoose.Schema(
|
||||||
lastMailFetch: { type: Date, default: new Date(0) },
|
lastMailFetch: { type: Date, default: new Date(0) },
|
||||||
lastAnswerFetch: { type: Date, default: new Date(0) },
|
lastAnswerFetch: { type: Date, default: new Date(0) },
|
||||||
user_id: { type: String, required: true, unique: true },
|
user_id: { type: String, required: true, unique: true },
|
||||||
|
badges: [String],
|
||||||
},
|
},
|
||||||
{ timestamps: { createdAt: false, updatedAt: true } }
|
{ 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);
|
module.exports = mongoose.model('User', User);
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,10 @@ const tokenAuth = require('server/middleware/tokenAuth');
|
||||||
|
|
||||||
const GetMail = require('./mail/Get');
|
const GetMail = require('./mail/Get');
|
||||||
const SendMail = require('./mail/Send');
|
const SendMail = require('./mail/Send');
|
||||||
|
const ReadMail = require('./mail/Read');
|
||||||
|
|
||||||
router.get('/', tokenAuth, GetMail);
|
router.get('/', tokenAuth, GetMail);
|
||||||
router.post('/', tokenAuth, SendMail);
|
router.post('/', tokenAuth, SendMail);
|
||||||
|
router.patch('/:mail_id', tokenAuth, ReadMail);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,15 @@ async function Get(req, res) {
|
||||||
|
|
||||||
// Fetch mail if expired
|
// Fetch mail if expired
|
||||||
if (Number(lastMailFetch) + config.mailExpires < Date.now()) {
|
if (Number(lastMailFetch) + config.mailExpires < Date.now()) {
|
||||||
const messages = await getAllMail({ username });
|
|
||||||
return res.send({ messages });
|
await getAllMail({ username })
|
||||||
} else {
|
}
|
||||||
const messages = await Mail.find({ receiver: username }).sort({
|
const messages = await Mail.find({ receiver: username }).sort({
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.send({ messages });
|
return res.send({ messages });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Get;
|
module.exports = Get;
|
||||||
|
|
|
||||||
12
server/routes/mail/Read.js
Normal file
12
server/routes/mail/Read.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
const Mail = require('server/db/models/Mail');
|
||||||
|
const config = require('server/config.json');
|
||||||
|
|
||||||
|
async function Read(req, res) {
|
||||||
|
const { mail_id } = req.params;
|
||||||
|
|
||||||
|
const cachedMail = await Mail.findByIdAndUpdate(mail_id, { read: true });
|
||||||
|
|
||||||
|
return cachedMail ? res.sendStatus(200) : res.status(404).send(config.errorNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Read;
|
||||||
|
|
@ -3,7 +3,6 @@ const router = express.Router();
|
||||||
|
|
||||||
const tokenAuth = require('server/middleware/tokenAuth');
|
const tokenAuth = require('server/middleware/tokenAuth');
|
||||||
|
|
||||||
|
|
||||||
const CreateAnswer = require('./question/CreateAnswer');
|
const CreateAnswer = require('./question/CreateAnswer');
|
||||||
const CreateAnswerComment = require('./question/CreateAnswerComment');
|
const CreateAnswerComment = require('./question/CreateAnswerComment');
|
||||||
const CreateComment = require('./question/CreateComment');
|
const CreateComment = require('./question/CreateComment');
|
||||||
|
|
@ -16,6 +15,8 @@ const EditAnswerCommentVote = require('./question/EditAnswerCommentVote');
|
||||||
const EditAnswerVote = require('./question/EditAnswerVote');
|
const EditAnswerVote = require('./question/EditAnswerVote');
|
||||||
const EditCommentVote = require('./question/EditCommentVote');
|
const EditCommentVote = require('./question/EditCommentVote');
|
||||||
const EditQuestion = require('./question/EditQuestion');
|
const EditQuestion = require('./question/EditQuestion');
|
||||||
|
const EditQuestionBody = require('./question/EditQuestionBody');
|
||||||
|
const EditQuestionBounty = require('./question/EditQuestionBounty');
|
||||||
const EditQuestionStatusClosed = require('./question/EditQuestionStatusClosed');
|
const EditQuestionStatusClosed = require('./question/EditQuestionStatusClosed');
|
||||||
const EditQuestionStatusProtected = require('./question/EditQuestionStatusProtected');
|
const EditQuestionStatusProtected = require('./question/EditQuestionStatusProtected');
|
||||||
const EditQuestionStatusReopened = require('./question/EditQuestionStatusReopened');
|
const EditQuestionStatusReopened = require('./question/EditQuestionStatusReopened');
|
||||||
|
|
@ -31,10 +32,11 @@ const GetQuestionVote = require('./question/GetQuestionVote');
|
||||||
const Search = require('./question/Search');
|
const Search = require('./question/Search');
|
||||||
|
|
||||||
router.get('/search', Search);
|
router.get('/search', Search);
|
||||||
|
|
||||||
router.post('/', tokenAuth, CreateQuestion);
|
router.post('/', tokenAuth, CreateQuestion);
|
||||||
router.get('/:question_id', GetQuestion);
|
router.get('/:question_id', GetQuestion);
|
||||||
router.patch('/:question_id', tokenAuth, EditQuestion);
|
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/close', tokenAuth, EditQuestionStatusClosed);
|
||||||
router.patch('/:question_id/protect', tokenAuth, EditQuestionStatusProtected);
|
router.patch('/:question_id/protect', tokenAuth, EditQuestionStatusProtected);
|
||||||
router.patch('/:question_id/reopen', tokenAuth, EditQuestionStatusReopened);
|
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.get('/:question_id/answers/:answer_id/comments', GetAnswerComments);
|
||||||
router.post('/:question_id/answers/:answer_id/comments', tokenAuth, CreateAnswerComment);
|
router.post('/:question_id/answers/:answer_id/comments', tokenAuth, CreateAnswerComment);
|
||||||
router.delete('/:question_id/answers/:answer_id/comments/:comment_id', tokenAuth, DeleteAnswerComment);
|
router.delete(
|
||||||
router.get('/:question_id/answers/:answer_id/comments/:comment_id/vote', tokenAuth, GetAnswerCommentVote);
|
'/:question_id/answers/:answer_id/comments/:comment_id',
|
||||||
router.patch('/:question_id/answers/:answer_id/comments/:comment_id/vote', tokenAuth, EditAnswerCommentVote);
|
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;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ async function CreateAnswer(req, res) {
|
||||||
|
|
||||||
if (question.status === 'closed') return res.status(403).send(config.errorForbidden);
|
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);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,14 @@ const createRequest = require('server/utils/api');
|
||||||
|
|
||||||
async function CreateQuestion(req, res) {
|
async function CreateQuestion(req, res) {
|
||||||
const user = req.user;
|
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 (!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`, {
|
const { success, question } = await createRequest('post', `/questions`, {
|
||||||
creator: user.username,
|
creator: user.username,
|
||||||
title,
|
title,
|
||||||
|
|
@ -17,7 +21,11 @@ async function CreateQuestion(req, res) {
|
||||||
|
|
||||||
if (!success) return res.status(500).send(config.errorGeneric);
|
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
|
// Increment user points by 1
|
||||||
await createRequest('patch', `/users/${user.username}/points`, {
|
await createRequest('patch', `/users/${user.username}/points`, {
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ async function EditAnswerAccepted(req, res) {
|
||||||
|
|
||||||
// Find question and verify that it exists
|
// Find question and verify that it exists
|
||||||
const question = await getQuestion(question_id);
|
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
|
// 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);
|
return res.status(403).send(config.errorForbidden);
|
||||||
|
}
|
||||||
|
|
||||||
// Patch question with BDPA server
|
// Patch question with BDPA server
|
||||||
const patchAnswer = await createRequest(
|
const patchAnswer = await createRequest(
|
||||||
|
|
@ -37,12 +39,22 @@ async function EditAnswerAccepted(req, res) {
|
||||||
await Question.findByIdAndUpdate(question_id, { hasAcceptedAnswer: true });
|
await Question.findByIdAndUpdate(question_id, { hasAcceptedAnswer: true });
|
||||||
|
|
||||||
// Increment points of answer creator
|
// Increment points of answer creator
|
||||||
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
|
if (hasBounty) {
|
||||||
operation: 'increment',
|
await User.findOneAndUpdate(
|
||||||
amount: 15,
|
{ username: cachedAnswer.creator },
|
||||||
});
|
{ $inc: { points: 15 + hasBounty } }
|
||||||
await User.findOneAndUpdate({ username: cachedAnswer.creator }, { $inc: { points: 15 } });
|
);
|
||||||
|
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);
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ async function EditAnswerCommentVote(req, res) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operation === 'downvote' && userLevel < 4) {
|
if (operation === 'downvote' && userLevel < 5) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ async function EditAnswerVote(req, res) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operation === 'downvote' && userLevel < 4) {
|
if (operation === 'downvote' && userLevel < 5) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -73,14 +73,10 @@ async function EditAnswerVote(req, res) {
|
||||||
amount: 1,
|
amount: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
await createRequest(
|
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
|
||||||
'patch',
|
operation: 'decrement',
|
||||||
`/users/${cachedAnswer.creator}/points`,
|
amount: 15,
|
||||||
{
|
});
|
||||||
operation: 'decrement',
|
|
||||||
amount: 15,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ vote: 'downvoted' });
|
return res.send({ vote: 'downvoted' });
|
||||||
}
|
}
|
||||||
|
|
@ -117,14 +113,10 @@ async function EditAnswerVote(req, res) {
|
||||||
docModel: 'Answer',
|
docModel: 'Answer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createRequest(
|
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
|
||||||
'patch',
|
operation: 'increment',
|
||||||
`/users/${cachedAnswer.creator}/points`,
|
amount: 15,
|
||||||
{
|
});
|
||||||
operation: 'increment',
|
|
||||||
amount: 15,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ vote: 'upvoted' });
|
return res.send({ vote: 'upvoted' });
|
||||||
}
|
}
|
||||||
|
|
@ -149,14 +141,10 @@ async function EditAnswerVote(req, res) {
|
||||||
docModel: 'Answer',
|
docModel: 'Answer',
|
||||||
});
|
});
|
||||||
|
|
||||||
await createRequest(
|
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
|
||||||
'patch',
|
operation: 'increment',
|
||||||
`/users/${cachedAnswer.creator}/points`,
|
amount: 10,
|
||||||
{
|
});
|
||||||
operation: 'increment',
|
|
||||||
amount: 10,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ vote: 'upvoted' });
|
return res.send({ vote: 'upvoted' });
|
||||||
} else if (operation === 'downvote') {
|
} else if (operation === 'downvote') {
|
||||||
|
|
@ -179,14 +167,10 @@ async function EditAnswerVote(req, res) {
|
||||||
amount: 1,
|
amount: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
await createRequest(
|
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
|
||||||
'patch',
|
operation: 'decrement',
|
||||||
`/users/${cachedAnswer.creator}/points`,
|
amount: 5,
|
||||||
{
|
});
|
||||||
operation: 'decrement',
|
|
||||||
amount: 5,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ vote: 'downvoted' });
|
return res.send({ vote: 'downvoted' });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ async function EditAnswerVote(req, res) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operation === 'downvote' && userLevel < 4) {
|
if (operation === 'downvote' && userLevel < 5) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
67
server/routes/question/EditQuestionBody.js
Normal file
67
server/routes/question/EditQuestionBody.js
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
const config = require('server/config.json');
|
||||||
|
const Question = require('server/db/models/Question');
|
||||||
|
const { getQuestion, refreshQuestion } = require('server/utils/question');
|
||||||
|
const createRequest = require('server/utils/api');
|
||||||
|
const getUserLevel = require('server/utils/getUserLevel');
|
||||||
|
|
||||||
|
async function EditQuestionStatusClosed(req, res) {
|
||||||
|
const { user } = req;
|
||||||
|
const { question_id } = req.params;
|
||||||
|
const { etext, etitle } = req.body;
|
||||||
|
let eObj = [etext, etitle];
|
||||||
|
// Verify user has required level
|
||||||
|
if (getUserLevel(user.points) < 7) {
|
||||||
|
return res.status(403).send(config.errorForbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cached question and make sure it exists
|
||||||
|
const question = await getQuestion(question_id);
|
||||||
|
if (!question) return res.status(404).send(config.errorNotFound);
|
||||||
|
|
||||||
|
// Toggle question vote
|
||||||
|
if (question.edit.length === 0) {
|
||||||
|
if (!etext || !etitle) {
|
||||||
|
return res.status(400).send(config.errorIncomplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Question.findByIdAndUpdate(question_id, {
|
||||||
|
editText: eObj,
|
||||||
|
$push: { edit: user.username },
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.sendStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (question.edit.includes(user.username)) {
|
||||||
|
await Question.findByIdAndUpdate(question_id, {
|
||||||
|
$pull: { edit: user.username },
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.sendStatus(200);
|
||||||
|
} else {
|
||||||
|
await Question.findByIdAndUpdate(question_id, {
|
||||||
|
$push: { close: user.username },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch question status if required
|
||||||
|
if (question.edit.length === 2) {
|
||||||
|
const question = await Question.findByIdAndUpdate(question_id, {
|
||||||
|
edit: [],
|
||||||
|
eObj,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { success } = await createRequest('patch', `/questions/${question_id}`, {
|
||||||
|
etext,
|
||||||
|
eObj,
|
||||||
|
});
|
||||||
|
|
||||||
|
return success
|
||||||
|
? res.send({ text: question.text })
|
||||||
|
: res.status(500).send(config.errorGeneric);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sendStatus(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EditQuestionStatusClosed;
|
||||||
31
server/routes/question/EditQuestionBounty.js
Normal file
31
server/routes/question/EditQuestionBounty.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
const Question = require('server/db/models/Question');
|
||||||
|
const User = require('server/db/models/User');
|
||||||
|
const createRequest = require('server/utils/api');
|
||||||
|
const getUserLevel = require('server/utils/getUserLevel');
|
||||||
|
const config = require('server/config.json');
|
||||||
|
|
||||||
|
async function EditQuestionBounty(req, res) {
|
||||||
|
const { user } = req.user;
|
||||||
|
const { question_id } = req.params;
|
||||||
|
const { bounty } = req.body;
|
||||||
|
|
||||||
|
if (!bounty) return res.status(400).send(config.errorIncomplete);
|
||||||
|
const userLevel = getUserLevel(user.points);
|
||||||
|
|
||||||
|
if (userLevel < 4 || user.points < bounty) {
|
||||||
|
return res.status(403).send(config.errorForbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Question.findByIdAndUpdate(question_id, { hasBounty: bounty });
|
||||||
|
|
||||||
|
await User.findOneAndUpdate(
|
||||||
|
{ username: cachedAnswer.creator },
|
||||||
|
{ $inc: { points: Math.abs(bounty) * -1 } }
|
||||||
|
);
|
||||||
|
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
|
||||||
|
operation: 'decrement',
|
||||||
|
amount: bounty,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = EditQuestionBounty;
|
||||||
|
|
@ -9,7 +9,7 @@ async function EditQuestionStatusClosed(req, res) {
|
||||||
const { question_id } = req.params;
|
const { question_id } = req.params;
|
||||||
|
|
||||||
// Verify user has required level
|
// Verify user has required level
|
||||||
if (getUserLevel(user.points) < 7) {
|
if (getUserLevel(user.points) < 9) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,11 +45,9 @@ async function EditQuestionStatusClosed(req, res) {
|
||||||
status: 'closed',
|
status: 'closed',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { success } = await createRequest(
|
const { success } = await createRequest('patch', `/questions/${question_id}`, {
|
||||||
'patch',
|
status: 'closed',
|
||||||
`/questions/${question_id}`,
|
});
|
||||||
{ status: 'closed' }
|
|
||||||
);
|
|
||||||
await Question.findByIdAndUpdate(question_id, { status: 'closed' });
|
await Question.findByIdAndUpdate(question_id, { status: 'closed' });
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ async function EditQuestionStatusProtected(req, res) {
|
||||||
const { question_id } = req.params;
|
const { question_id } = req.params;
|
||||||
|
|
||||||
// Verify user has required level
|
// Verify user has required level
|
||||||
if (getUserLevel(user.points) < 6) {
|
if (getUserLevel(user.points) < 8) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
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);
|
if (!question) return res.status(404).send(config.errorNotFound);
|
||||||
|
|
||||||
// Verify question does not have incompatible status
|
// Verify question does not have incompatible status
|
||||||
if (
|
if (question.status === 'closed' || question.status === 'protected') {
|
||||||
question.status === 'closed' ||
|
|
||||||
question.status === 'protected'
|
|
||||||
) {
|
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'This question is already closed or protected.',
|
error: 'This question is already closed or protected.',
|
||||||
|
|
@ -48,11 +45,11 @@ async function EditQuestionStatusProtected(req, res) {
|
||||||
status: 'protected',
|
status: 'protected',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { success } = await createRequest(
|
await User.findOneAndUpdate({ username: creator }, { $push: { badges: 'Protected' } });
|
||||||
'patch',
|
|
||||||
`/questions/${question_id}`,
|
const { success } = await createRequest('patch', `/questions/${question_id}`, {
|
||||||
{ status: 'protected' }
|
status: 'protected',
|
||||||
);
|
});
|
||||||
await Question.findByIdAndUpdate(question_id, { status: 'protected' });
|
await Question.findByIdAndUpdate(question_id, { status: 'protected' });
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const config = require('server/config.json');
|
const config = require('server/config.json');
|
||||||
const Question = require('server/db/models/Question');
|
const Question = require('server/db/models/Question');
|
||||||
const { getQuestion, refreshQuestion } = require('server/utils/question');
|
const { getQuestion } = require('server/utils/question');
|
||||||
const getUserLevel = require('server/utils/getUserLevel');
|
const getUserLevel = require('server/utils/getUserLevel');
|
||||||
const createRequest = require('server/utils/api');
|
const createRequest = require('server/utils/api');
|
||||||
|
|
||||||
|
|
@ -9,7 +9,7 @@ async function EditQuestionStatusReopened(req, res) {
|
||||||
const { question_id } = req.params;
|
const { question_id } = req.params;
|
||||||
|
|
||||||
// Verify user has permissions
|
// Verify user has permissions
|
||||||
if (getUserLevel(user.points) < 7) {
|
if (getUserLevel(user.points) < 9) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
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);
|
if (!question) return res.status(404).send(config.errorNotFound);
|
||||||
|
|
||||||
// Make sure that question has compatible status
|
// Make sure that question has compatible status
|
||||||
if (
|
if (question.status === 'protected' || question.status === 'open') {
|
||||||
question.status === 'protected' ||
|
|
||||||
question.status === 'open'
|
|
||||||
) {
|
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'This question is already open.',
|
error: 'This question is already open.',
|
||||||
|
|
@ -48,11 +45,11 @@ async function EditQuestionStatusReopened(req, res) {
|
||||||
status: 'open',
|
status: 'open',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { success } = await createRequest(
|
await User.findOneAndUpdate({ username: creator }, { $push: { badges: 'Zombie' } });
|
||||||
'patch',
|
|
||||||
`/questions/${question_id}`,
|
const { success } = await createRequest('patch', `/questions/${question_id}`, {
|
||||||
{ status: 'open' }
|
status: 'open',
|
||||||
);
|
});
|
||||||
await Question.findByIdAndUpdate(question_id, { status: 'open' });
|
await Question.findByIdAndUpdate(question_id, { status: 'open' });
|
||||||
|
|
||||||
return success
|
return success
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ async function EditQuestionVote(req, res) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (operation === 'downvote' && userLevel < 4) {
|
if (operation === 'downvote' && userLevel < 5) {
|
||||||
return res.status(403).send(config.errorForbidden);
|
return res.status(403).send(config.errorForbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,10 +75,7 @@ async function EditQuestionVote(req, res) {
|
||||||
operation: 'decrement',
|
operation: 'decrement',
|
||||||
amount: 6,
|
amount: 6,
|
||||||
});
|
});
|
||||||
await User.findOneAndUpdate(
|
await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: -6 } });
|
||||||
{ username: question.creator },
|
|
||||||
{ $inc: { points: -6 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decrement the user's points
|
// Decrement the user's points
|
||||||
await createRequest('patch', `/users/${user.username}/points`, {
|
await createRequest('patch', `/users/${user.username}/points`, {
|
||||||
|
|
@ -90,19 +87,12 @@ async function EditQuestionVote(req, res) {
|
||||||
return res.send({ vote: 'downvoted' });
|
return res.send({ vote: 'downvoted' });
|
||||||
} else {
|
} else {
|
||||||
// Decrement the question creator's points
|
// Decrement the question creator's points
|
||||||
const { success } = await createRequest(
|
const { success } = await createRequest('patch', `/users/${question.creator}/points`, {
|
||||||
'patch',
|
operation: 'decrement',
|
||||||
`/users/${question.creator}/points`,
|
amount: 5,
|
||||||
{
|
});
|
||||||
operation: 'decrement',
|
|
||||||
amount: 5,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await User.findOneAndUpdate(
|
await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: -5 } });
|
||||||
{ username: question.creator },
|
|
||||||
{ $inc: { points: -5 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success) return res.status(500).send(config.errorGeneric);
|
if (!success) return res.status(500).send(config.errorGeneric);
|
||||||
|
|
||||||
|
|
@ -139,10 +129,7 @@ async function EditQuestionVote(req, res) {
|
||||||
operation: 'increment',
|
operation: 'increment',
|
||||||
amount: 6,
|
amount: 6,
|
||||||
});
|
});
|
||||||
await User.findOneAndUpdate(
|
await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: 6 } });
|
||||||
{ username: question.creator },
|
|
||||||
{ $inc: { points: 6 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ vote: 'upvoted' });
|
return res.send({ vote: 'upvoted' });
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -151,10 +138,7 @@ async function EditQuestionVote(req, res) {
|
||||||
operation: 'increment',
|
operation: 'increment',
|
||||||
amount: 1,
|
amount: 1,
|
||||||
});
|
});
|
||||||
await User.findOneAndUpdate(
|
await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: 1 } });
|
||||||
{ username: question.creator },
|
|
||||||
{ $inc: { points: 1 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Increment the user's points
|
// Increment the user's points
|
||||||
await createRequest('patch', `/users/${user.username}/points`, {
|
await createRequest('patch', `/users/${user.username}/points`, {
|
||||||
|
|
@ -188,10 +172,7 @@ async function EditQuestionVote(req, res) {
|
||||||
operation: 'increment',
|
operation: 'increment',
|
||||||
amount: 5,
|
amount: 5,
|
||||||
});
|
});
|
||||||
await User.findOneAndUpdate(
|
await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: 5 } });
|
||||||
{ username: question.creator },
|
|
||||||
{ $inc: { points: 5 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ vote: 'upvoted' });
|
return res.send({ vote: 'upvoted' });
|
||||||
} else if (operation === 'downvote') {
|
} else if (operation === 'downvote') {
|
||||||
|
|
@ -218,10 +199,7 @@ async function EditQuestionVote(req, res) {
|
||||||
operation: 'decrement',
|
operation: 'decrement',
|
||||||
amount: 1,
|
amount: 1,
|
||||||
});
|
});
|
||||||
await User.findOneAndUpdate(
|
await User.findOneAndUpdate({ username: question.creator }, { $inc: { points: -1 } });
|
||||||
{ username: question.creator },
|
|
||||||
{ $inc: { points: -1 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Decrement the user's points
|
// Decrement the user's points
|
||||||
await createRequest('patch', `/users/${user.username}/points`, {
|
await createRequest('patch', `/users/${user.username}/points`, {
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,59 @@
|
||||||
const createRequest = require('server/utils/api');
|
|
||||||
const config = require('server/config.json');
|
const config = require('server/config.json');
|
||||||
|
|
||||||
const Question = require('server/db/models/Question');
|
const Question = require('server/db/models/Question');
|
||||||
|
|
||||||
async function Search(req, res) {
|
async function Search(req, res) {
|
||||||
const { success, questions } = await createRequest(
|
const { creator, title, text, tags, createdAt, sort } = req.query;
|
||||||
'get',
|
|
||||||
`/questions/search`,
|
|
||||||
req.query
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success) return res.status(500).send(config.errorGeneric);
|
let searchQuery = {};
|
||||||
|
if (creator) {
|
||||||
|
searchQuery['creator'] = {
|
||||||
|
$regex: creator,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (title) {
|
||||||
|
searchQuery['title'] = {
|
||||||
|
$regex: title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
searchQuery['text'] = {
|
||||||
|
$regex: text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (tags) {
|
||||||
|
searchQuery['tags'] = { $all: tags };
|
||||||
|
}
|
||||||
|
if (createdAt) {
|
||||||
|
searchQuery['createdAt'] = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
// Patch search results to database
|
let sortQuery = [[createdAt, 'desc']];
|
||||||
const questionSet = await Promise.all(
|
switch (sort) {
|
||||||
questions
|
case 'u':
|
||||||
.map((question) => ({
|
sortQuery = [['upvotes', 'desc']];
|
||||||
id: question.question_id,
|
break;
|
||||||
...question,
|
case 'uvc':
|
||||||
}))
|
sortQuery = [
|
||||||
.map(async (question) => {
|
['upvotes', 'desc'],
|
||||||
return Question.findByIdAndUpdate(question.id, question, {
|
['view', 'desc'],
|
||||||
upsert: true,
|
['comments', 'desc'],
|
||||||
});
|
];
|
||||||
})
|
break;
|
||||||
);
|
case 'uvac':
|
||||||
|
sortQuery = [
|
||||||
|
['upvotes', 'desc'],
|
||||||
|
['view', 'desc'],
|
||||||
|
['answers', 'desc'],
|
||||||
|
['comments', 'desc'],
|
||||||
|
];
|
||||||
|
searchQuery['hasAcceptedAnswer'] = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return res.send({ questions: questionSet.filter(question => question) });
|
const questions = await Question.find(searchQuery).sort(sortQuery);
|
||||||
|
|
||||||
|
return res.send({ questions });
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Search;
|
module.exports = Search;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ const basicAuth = require('server/middleware/basicAuth');
|
||||||
const tokenAuth = require('server/middleware/tokenAuth');
|
const tokenAuth = require('server/middleware/tokenAuth');
|
||||||
|
|
||||||
const Answers = require('./user/Answers');
|
const Answers = require('./user/Answers');
|
||||||
|
const GetBadges = require('./user/GetBadges');
|
||||||
const Delete = require('./user/Delete');
|
const Delete = require('./user/Delete');
|
||||||
const Edit = require('./user/Edit');
|
const Edit = require('./user/Edit');
|
||||||
const GetUser = require('./user/GetUser');
|
const GetUser = require('./user/GetUser');
|
||||||
|
|
@ -20,6 +21,7 @@ router.post('/', Register);
|
||||||
router.patch('/', tokenAuth, Edit);
|
router.patch('/', tokenAuth, Edit);
|
||||||
router.get('/questions', tokenAuth, Questions);
|
router.get('/questions', tokenAuth, Questions);
|
||||||
router.get('/answers', tokenAuth, Answers);
|
router.get('/answers', tokenAuth, Answers);
|
||||||
|
router.get('/badges', tokenAuth, GetBadges);
|
||||||
router.post('/login', basicAuth, Login);
|
router.post('/login', basicAuth, Login);
|
||||||
router.get('/remember', tokenAuth, Remember);
|
router.get('/remember', tokenAuth, Remember);
|
||||||
router.delete('/logout', tokenAuth, Logout);
|
router.delete('/logout', tokenAuth, Logout);
|
||||||
|
|
|
||||||
24
server/routes/user/GetBadges.js
Normal file
24
server/routes/user/GetBadges.js
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
const Badge = require('server/db/models/Badge');
|
||||||
|
|
||||||
|
async function GetBadges(req, res) {
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
|
const badges = await Badge.find();
|
||||||
|
|
||||||
|
const response = badges.reduce(
|
||||||
|
(acc, badge) => {
|
||||||
|
if (user.badges.includes(badge.title)) {
|
||||||
|
acc.obtained.push(badge);
|
||||||
|
} else {
|
||||||
|
acc.unobtained.push(badge);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ obtained: [], unobtained: [] }
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.send({ badges: response });
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = GetBadges;
|
||||||
|
|
@ -2,6 +2,7 @@ const createRequest = require('server/utils/api');
|
||||||
const config = require('server/config.json');
|
const config = require('server/config.json');
|
||||||
const User = require('server/db/models/User');
|
const User = require('server/db/models/User');
|
||||||
const getUserLevel = require('server/utils/getUserLevel');
|
const getUserLevel = require('server/utils/getUserLevel');
|
||||||
|
const Badge = require('server/db/models/Badge');
|
||||||
|
|
||||||
async function GetUser(req, res) {
|
async function GetUser(req, res) {
|
||||||
const { username } = req.params;
|
const { username } = req.params;
|
||||||
|
|
@ -27,12 +28,34 @@ async function GetUser(req, res) {
|
||||||
new: true,
|
new: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const badges = await Badge.find({ title: { $in: user.badges } });
|
||||||
|
|
||||||
|
const badgeCount = badges.reduce(
|
||||||
|
(acc, badge) => {
|
||||||
|
if (badge.rank === 'Gold') {
|
||||||
|
acc.gold = acc.gold++;
|
||||||
|
} else if (badge.rank === 'Silver') {
|
||||||
|
acc.silver = acc.silver++;
|
||||||
|
} else if (badge.rank === 'Bronze') {
|
||||||
|
acc.bronze = acc.bronze++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gold: 0,
|
||||||
|
silver: 0,
|
||||||
|
bronze: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
user: {
|
user: {
|
||||||
username: newUser.username,
|
username: newUser.username,
|
||||||
email: newUser.email,
|
email: newUser.email,
|
||||||
points: newUser.points,
|
points: newUser.points,
|
||||||
level: getUserLevel(newUser.points),
|
level: getUserLevel(newUser.points),
|
||||||
|
badgeCount,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const mongoose = require('mongoose');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
const Token = require('server/db/models/Token');
|
const Token = require('server/db/models/Token');
|
||||||
|
const Badge = require('server/db/models/Badge');
|
||||||
const getUserLevel = require('server/utils/getUserLevel');
|
const getUserLevel = require('server/utils/getUserLevel');
|
||||||
|
|
||||||
async function Login(req, res) {
|
async function Login(req, res) {
|
||||||
|
|
@ -20,6 +21,27 @@ async function Login(req, res) {
|
||||||
user: user.username,
|
user: user.username,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const badges = await Badge.find({ title: { $in: user.badges } });
|
||||||
|
|
||||||
|
const badgeCount = badges.reduce(
|
||||||
|
(acc, badge) => {
|
||||||
|
if (badge.rank === 'Gold') {
|
||||||
|
acc.gold = acc.gold + 1;
|
||||||
|
} else if (badge.rank === 'Silver') {
|
||||||
|
acc.silver = acc.silver + 1;
|
||||||
|
} else if (badge.rank === 'Bronze') {
|
||||||
|
acc.bronze = acc.bronze + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gold: 0,
|
||||||
|
silver: 0,
|
||||||
|
bronze: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
token: token.token,
|
token: token.token,
|
||||||
user: {
|
user: {
|
||||||
|
|
@ -27,7 +49,8 @@ async function Login(req, res) {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
points: user.points,
|
points: user.points,
|
||||||
level: getUserLevel(user.points),
|
level: getUserLevel(user.points),
|
||||||
}
|
badgeCount,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,38 @@
|
||||||
const getUserLevel = require('server/utils/getUserLevel');
|
const getUserLevel = require('server/utils/getUserLevel');
|
||||||
|
const Badge = require('../../db/models/Badge');
|
||||||
|
|
||||||
async function Remember(req, res) {
|
async function Remember(req, res) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
|
|
||||||
|
const badges = await Badge.find({ title: { $in: user.badges } });
|
||||||
|
|
||||||
|
const badgeCount = badges.reduce(
|
||||||
|
(acc, badge) => {
|
||||||
|
if (badge.rank === 'Gold') {
|
||||||
|
acc.gold = acc.gold + 1;
|
||||||
|
} else if (badge.rank === 'Silver') {
|
||||||
|
acc.silver = acc.silver + 1;
|
||||||
|
} else if (badge.rank === 'Bronze') {
|
||||||
|
acc.bronze = acc.bronze + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gold: 0,
|
||||||
|
silver: 0,
|
||||||
|
bronze: 0,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
user: {
|
user: {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
points: user.points,
|
points: user.points,
|
||||||
level: getUserLevel(user.points),
|
level: getUserLevel(user.points),
|
||||||
}
|
badgeCount,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
server/utils/badges/answerBadges.js
Normal file
17
server/utils/badges/answerBadges.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
function calculateAnswerBadges(points) {
|
||||||
|
let badges = [];
|
||||||
|
|
||||||
|
if (points >= 100) {
|
||||||
|
badges.push('Great Answer');
|
||||||
|
}
|
||||||
|
if (points >= 25) {
|
||||||
|
badges.push('Good Answer');
|
||||||
|
}
|
||||||
|
if (points >= 10) {
|
||||||
|
badges.push('Nice Answer');
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = calculateAnswerBadges;
|
||||||
17
server/utils/badges/questionBadges.js
Normal file
17
server/utils/badges/questionBadges.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
function calculateQuestionBadges(points) {
|
||||||
|
let badges = [];
|
||||||
|
|
||||||
|
if (points >= 100) {
|
||||||
|
badges.push('Great Question');
|
||||||
|
}
|
||||||
|
if (points >= 25) {
|
||||||
|
badges.push('Good Question');
|
||||||
|
}
|
||||||
|
if (points >= 10) {
|
||||||
|
badges.push('Nice Question');
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = calculateQuestionBadges;
|
||||||
17
server/utils/badges/userBadges.js
Normal file
17
server/utils/badges/userBadges.js
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
function calculateUserBadges(points) {
|
||||||
|
let badges = [];
|
||||||
|
|
||||||
|
if (points >= 10000) {
|
||||||
|
badges.push('Socratic');
|
||||||
|
}
|
||||||
|
if (points >= 3000) {
|
||||||
|
badges.push('Inquisitive');
|
||||||
|
}
|
||||||
|
if (points >= 100) {
|
||||||
|
badges.push('Curious');
|
||||||
|
}
|
||||||
|
|
||||||
|
return badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = calculateUserBadges;
|
||||||
|
|
@ -1,20 +1,14 @@
|
||||||
function getUserLevel(points) {
|
function getUserLevel(points) {
|
||||||
if (points > 10000)
|
if (points > 10000) return 9;
|
||||||
return 7;
|
else if (points > 3000) return 8;
|
||||||
else if (points > 3000)
|
else if (points > 2000) return 7;
|
||||||
return 6;
|
else if (points > 1000) return 6;
|
||||||
else if (points > 1000)
|
else if (points > 125) return 5;
|
||||||
return 5;
|
else if (points > 75) return 4;
|
||||||
else if (points > 125)
|
else if (points > 50) return 3;
|
||||||
return 4;
|
else if (points > 15) return 2;
|
||||||
else if (points > 50)
|
else if (points > 0) return 1;
|
||||||
return 3;
|
else return 0;
|
||||||
else if (points > 15)
|
|
||||||
return 2;
|
|
||||||
else if (points > 0)
|
|
||||||
return 1;
|
|
||||||
else
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = getUserLevel;
|
module.exports = getUserLevel;
|
||||||
|
|
|
||||||
Reference in a new issue