Merge branch 'dev' into main
This commit is contained in:
commit
273792eb29
34 changed files with 378 additions and 194 deletions
|
|
@ -22,6 +22,7 @@
|
|||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-time-ago": "^7.2.1",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"superagent": "^8.0.0",
|
||||
"superagent-throttle": "^1.0.1",
|
||||
"universal-cookie": "^4.0.4",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Typography } from '@mui/material';
|
||||
import Gravatar from 'react-gravatar';
|
||||
import { Typography, Avatar, Box } from '@mui/material';
|
||||
import { getGravatarURL } from 'services/getGravatarURL';
|
||||
import ReactTimeAgo from 'react-time-ago';
|
||||
|
||||
export default function CreationInfoTag({
|
||||
|
|
@ -8,18 +8,21 @@ export default function CreationInfoTag({
|
|||
text = 'asked',
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<Gravatar email={email} size={20} style={{ borderRadius: '15%' }} />
|
||||
<Box display='flex' alignItems='center'>
|
||||
<Avatar
|
||||
sx={{ width: 20, height: 20 }}
|
||||
src={getGravatarURL(email)}
|
||||
/>
|
||||
<Typography display='inline' m={1}>
|
||||
{username}
|
||||
</Typography>
|
||||
<Typography display='inline'>
|
||||
<b>Level {level}</b>
|
||||
<b>{level}</b>
|
||||
</Typography>
|
||||
<Typography display='inline' m={1} variant='body2'>
|
||||
{text}{' '}
|
||||
<ReactTimeAgo date={new Date(createdAt)} locale='en-US' />
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,26 @@
|
|||
import React, {useEffect} from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Button, TextField, Typography } from '@mui/material';
|
||||
import { useForm } from '../contexts/FormContext';
|
||||
import { useForm } from '../contexts/FormContext';
|
||||
|
||||
export default function Form({ formik, fields }) {
|
||||
const {setContent} = useForm()
|
||||
|
||||
useEffect(()=>{
|
||||
setContent("")
|
||||
}, [])
|
||||
const { setContent } = useForm();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setContent('');
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
function handleChange(event){
|
||||
|
||||
if(event.target.id === "text"){
|
||||
|
||||
|
||||
setContent(event.target.value)
|
||||
|
||||
function handleChange(event) {
|
||||
if (event.target.id === 'text') {
|
||||
setContent(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<form onSubmit={formik.handleSubmit} onChange={handleChange}>
|
||||
{fields.map((field) => (
|
||||
<span key={field.id}>
|
||||
<Typography>{field.title}</Typography>
|
||||
<TextField {...field} fullWidth/>
|
||||
<TextField {...field} fullWidth />
|
||||
</span>
|
||||
))}
|
||||
|
||||
|
|
@ -43,7 +33,6 @@ export default function Form({ formik, fields }) {
|
|||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
|
|||
import { CreationInfoTag } from 'controllers';
|
||||
|
||||
export default function ListQuestion({
|
||||
question: {
|
||||
data: {
|
||||
answers,
|
||||
createdAt,
|
||||
creator,
|
||||
|
|
@ -14,9 +14,8 @@ export default function ListQuestion({
|
|||
title,
|
||||
upvotes,
|
||||
views,
|
||||
}
|
||||
},
|
||||
}) {
|
||||
|
||||
return (
|
||||
<span>
|
||||
<ListItem disablePadding>
|
||||
|
|
@ -26,8 +25,12 @@ export default function ListQuestion({
|
|||
<Typography variant='body1'>
|
||||
{upvotes - downvotes} votes
|
||||
</Typography>
|
||||
<Typography variant='body1'>{answers} answers</Typography>
|
||||
<Typography variant='body1'>{views} views</Typography>
|
||||
<Typography variant='body1'>
|
||||
{answers} answers
|
||||
</Typography>
|
||||
<Typography variant='body1'>
|
||||
{views} views
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid item xs={10}>
|
||||
|
|
@ -40,7 +43,9 @@ export default function ListQuestion({
|
|||
>
|
||||
[{status}] {title}
|
||||
</Typography>
|
||||
<Typography noWrap variant='body1'>{text}</Typography>
|
||||
<Typography noWrap variant='body1'>
|
||||
{text}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ListItem>
|
||||
|
|
|
|||
|
|
@ -3,18 +3,28 @@ import {
|
|||
AccordionSummary,
|
||||
Typography,
|
||||
AccordionDetails,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import ReactTimeAgo from 'react-time-ago';
|
||||
|
||||
export default function MailUnit(sender, createdAt, subject, text) {
|
||||
export default function MailUnit({ sender, createdAt, subject, text }) {
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography>Subject: {subject}</Typography>
|
||||
<Typography>From {sender} at {new Date(createdAt * 1000)}</Typography>
|
||||
<Box style={{ width: '100%' }} display={'flex'}>
|
||||
<Typography>From {sender}</Typography>
|
||||
<span style={{ width: '1vw' }} />
|
||||
<ReactTimeAgo date={new Date(createdAt)} locale='en-US' />
|
||||
<span style={{ width: '2vw' }} />
|
||||
<Typography>
|
||||
<b>{subject}</b>
|
||||
</Typography>
|
||||
</Box>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography variant='body1'>{text}</Typography>
|
||||
<ReactMarkdown>{text}</ReactMarkdown>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,34 @@
|
|||
import { useForm } from "contexts"
|
||||
import { useState } from "react"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import {Paper, Typography, Button} from '@mui/material'
|
||||
export default function MdPreview(){
|
||||
const [preview, setPreview] = useState(false)
|
||||
const {content} = useForm()
|
||||
import { useForm } from 'contexts';
|
||||
import { useState } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Paper, Typography, Button } from '@mui/material';
|
||||
|
||||
function handleClick(){
|
||||
setPreview(!preview)
|
||||
export default function MdPreview() {
|
||||
const [preview, setPreview] = useState(false);
|
||||
const { content } = useForm();
|
||||
|
||||
function handleClick() {
|
||||
setPreview(!preview);
|
||||
}
|
||||
return(
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button fullWidth
|
||||
onClick = {handleClick}
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={handleClick}
|
||||
type='submit'
|
||||
variant='contained'
|
||||
color='primary'
|
||||
sx={{ mt: '1vh' }}>
|
||||
Show Markdown Preview
|
||||
sx={{ mt: '1vh' }}
|
||||
>
|
||||
Show Markdown Preview
|
||||
</Button>
|
||||
|
||||
{preview ? <Paper variant = "outlined"><Typography variant = "h5"> Text Preview: </Typography><ReactMarkdown children={content}/></Paper> : null}
|
||||
{preview ? (
|
||||
<Paper sx={{ margin: '1vh 0', padding: '1vh' }}>
|
||||
<ReactMarkdown children={content} />
|
||||
</Paper>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
34
client/src/components/ModeToggle.jsx
Normal file
34
client/src/components/ModeToggle.jsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { IconButton } from '@mui/material';
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import { useMode } from 'contexts';
|
||||
|
||||
export default function ModeToggle() {
|
||||
const { mode, setMode } = useMode();
|
||||
|
||||
const onClick = () => {
|
||||
setMode(() => (mode === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
if (mode === 'light') {
|
||||
return (
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={onClick}
|
||||
sx={{ margin: '0 1vh' }}
|
||||
>
|
||||
<LightModeIcon />
|
||||
</IconButton>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<IconButton
|
||||
color='inherit'
|
||||
onClick={onClick}
|
||||
sx={{ margin: '0 1vh' }}
|
||||
>
|
||||
<DarkModeIcon />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,55 +6,41 @@ import {
|
|||
Typography,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import Gravatar from 'react-gravatar';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Logo from 'assets/bdpa-logo.svg';
|
||||
import { SearchBar } from 'components';
|
||||
import { ModeToggle, Profile } from 'components';
|
||||
import { SearchBar } from 'controllers';
|
||||
import MailIcon from '@mui/icons-material/Mail';
|
||||
|
||||
export default function Navbar({ logout, userData }) {
|
||||
function NavbarControls() {
|
||||
return userData.username ? (
|
||||
<>
|
||||
<Typography variant="button">Level: <b> {userData.level} </b> Points: <b> {userData.points}</b></Typography>
|
||||
<Button
|
||||
<IconButton
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/mail'
|
||||
sx={{ margin: '0 1vh' }}
|
||||
>
|
||||
Mail
|
||||
</Button>
|
||||
<MailIcon />
|
||||
</IconButton>
|
||||
<Profile userData={userData} sx={{ margin: '1vh 1vh' }} />
|
||||
<Button
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/dashboard'
|
||||
>
|
||||
Account
|
||||
</Button>
|
||||
<Button
|
||||
color='inherit'
|
||||
to='/login'
|
||||
onClick={logout}
|
||||
sx={{ margin: '0 1vh' }}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
<IconButton component={Link} to='/'>
|
||||
<Gravatar size={40} email={userData.email} />
|
||||
</IconButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/users/login'
|
||||
|
||||
>
|
||||
<Button color='inherit' component={Link} to='/users/login'>
|
||||
Login
|
||||
</Button>
|
||||
<Button
|
||||
color='inherit'
|
||||
component={Link}
|
||||
to='/users/register'
|
||||
|
||||
>
|
||||
<Button color='inherit' component={Link} to='/users/register'>
|
||||
Register
|
||||
</Button>
|
||||
</>
|
||||
|
|
@ -63,7 +49,11 @@ export default function Navbar({ logout, userData }) {
|
|||
return (
|
||||
<AppBar position='static'>
|
||||
<Toolbar>
|
||||
<IconButton component={Link} to='/'>
|
||||
<IconButton
|
||||
component={Link}
|
||||
to='/'
|
||||
sx={{ margin: '0 1vw 0 0 ' }}
|
||||
>
|
||||
<img src={Logo} alt='bdpa logo' width='40' height='40' />
|
||||
</IconButton>
|
||||
<Typography variant='h6' component='div'>
|
||||
|
|
@ -71,6 +61,7 @@ export default function Navbar({ logout, userData }) {
|
|||
</Typography>
|
||||
<SearchBar />
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<ModeToggle />
|
||||
<NavbarControls />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function PaginationEngine({ components, page, count }) {
|
||||
export default function PaginationEngine({
|
||||
component: Component,
|
||||
data,
|
||||
page,
|
||||
count,
|
||||
}) {
|
||||
const [bounds, setBounds] = useState({
|
||||
first: 0,
|
||||
last: 0,
|
||||
|
|
@ -12,5 +17,7 @@ export default function PaginationEngine({ components, page, count }) {
|
|||
|
||||
useEffect(calculateDataset, [page, count]);
|
||||
|
||||
return components.slice(bounds.first, bounds.last);
|
||||
return data
|
||||
.slice(bounds.first, bounds.last)
|
||||
.map((obj) => <Component data={obj} key={obj.id} />);
|
||||
}
|
||||
|
|
|
|||
16
client/src/components/Profile.jsx
Normal file
16
client/src/components/Profile.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Typography, IconButton, Avatar } from '@mui/material';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getGravatarURL } from 'services/getGravatarURL';
|
||||
|
||||
export default function Profile({ userData }) {
|
||||
return (
|
||||
<>
|
||||
<IconButton component={Link} to='/dashboard'>
|
||||
<Avatar size={40} src={getGravatarURL(userData.email)} />
|
||||
</IconButton>
|
||||
<Typography variant='body1'>
|
||||
<b>{userData.level}</b> - {userData.points}
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { alpha, styled } from '@mui/material/styles';
|
||||
import { InputBase } from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useState } from 'react';
|
||||
|
||||
const Search = styled('div')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
|
|
@ -43,13 +44,19 @@ const Input = styled(InputBase)(({ theme }) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
export default function SearchBar() {
|
||||
export default function SearchBar({ value, onChange, onSubmit }) {
|
||||
return (
|
||||
<Search>
|
||||
<SearchIconWrapper>
|
||||
<SearchIcon />
|
||||
</SearchIconWrapper>
|
||||
<Input placeholder='Search…' />
|
||||
</Search>
|
||||
<form onSubmit={onSubmit}>
|
||||
<Search>
|
||||
<SearchIconWrapper>
|
||||
<SearchIcon />
|
||||
</SearchIconWrapper>
|
||||
<Input
|
||||
placeholder='Search…'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Search>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import PaginationEngine from './PaginationEngine';
|
|||
import PaginatedList from './PaginatedList';
|
||||
import SearchBar from './SearchBar';
|
||||
import MdPreview from './MdPreview';
|
||||
import Profile from './Profile';
|
||||
import ModeToggle from './ModeToggle';
|
||||
|
||||
export {
|
||||
CreationInfoTag,
|
||||
|
|
@ -17,5 +19,7 @@ export {
|
|||
PaginationEngine,
|
||||
PaginatedList,
|
||||
SearchBar,
|
||||
MdPreview
|
||||
MdPreview,
|
||||
Profile,
|
||||
ModeToggle,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -37,21 +37,27 @@ export default function Buffet() {
|
|||
const { setError } = useError();
|
||||
|
||||
useEffect(() => {
|
||||
loadQuestions(0);
|
||||
loadQuestions(sort);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
loadQuestions(sort);
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
async function loadQuestions(newSort) {
|
||||
if (newSort >= 0) {
|
||||
const request = await searchQuestions({
|
||||
sort: sortObjArr[newSort].sort,
|
||||
match: sortObjArr[newSort].match,
|
||||
});
|
||||
const request = await searchQuestions({
|
||||
sort: sortObjArr[newSort].sort,
|
||||
match: sortObjArr[newSort].match,
|
||||
});
|
||||
|
||||
if (request.error) {
|
||||
setError(request.error);
|
||||
} else {
|
||||
setQuestionSet(request.questions);
|
||||
}
|
||||
if (request.error) {
|
||||
setError(request.error);
|
||||
} else {
|
||||
setQuestionSet(request.questions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,12 +73,6 @@ export default function Buffet() {
|
|||
setCurrentPage(() => value);
|
||||
}
|
||||
|
||||
function getComponents() {
|
||||
return questionSet.map((question) => (
|
||||
<ListQuestion question={question} key={question.id} />
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
|
|
@ -109,7 +109,8 @@ export default function Buffet() {
|
|||
</Box>
|
||||
<List sx={{ pl: 2, pr: 2 }}>
|
||||
<PaginationEngine
|
||||
components={getComponents()}
|
||||
component={ListQuestion}
|
||||
data={questionSet}
|
||||
page={currentPage}
|
||||
count={count}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { useState } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Box, CssBaseline } from '@mui/material';
|
||||
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||
|
||||
import { useMode } from 'contexts';
|
||||
import { Navbar } from 'controllers';
|
||||
|
||||
export default function Layout() {
|
||||
const [mode, setMode] = useState('light');
|
||||
const { mode } = useMode();
|
||||
|
||||
const theme = createTheme({
|
||||
palette: { mode },
|
||||
breakpoints: {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { Card, CardContent } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { Inbox, SendMail } from 'controllers';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useForm } from 'contexts';
|
||||
import { MdPreview } from 'components';
|
||||
|
||||
export default function Mail() {
|
||||
|
|
@ -10,7 +8,7 @@ export default function Mail() {
|
|||
<Card>
|
||||
<CardContent>
|
||||
<SendMail />
|
||||
<MdPreview/>
|
||||
<MdPreview />
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<Inbox />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { UserProvider, FormProvider, ErrorProvider } from '.';
|
||||
import { UserProvider, FormProvider, ErrorProvider, ModeProvider } from '.';
|
||||
|
||||
export default function ContextProvider({ children }) {
|
||||
return (
|
||||
<UserProvider>
|
||||
<FormProvider>
|
||||
<ErrorProvider>{children}</ErrorProvider>
|
||||
<ModeProvider>
|
||||
<ErrorProvider>{children}</ErrorProvider>
|
||||
</ModeProvider>
|
||||
</FormProvider>
|
||||
</UserProvider>
|
||||
);
|
||||
|
|
|
|||
15
client/src/contexts/ModeContext.js
Normal file
15
client/src/contexts/ModeContext.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
const ModeContext = createContext();
|
||||
|
||||
export default function ModeProvider({ children }) {
|
||||
const [mode, setMode] = useState('light');
|
||||
|
||||
return (
|
||||
<ModeContext.Provider value={{ mode, setMode }}>
|
||||
{children}
|
||||
</ModeContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useMode = () => useContext(ModeContext);
|
||||
|
|
@ -4,6 +4,7 @@ import QuestionProvider, { useQuestion } from './QuestionContext';
|
|||
import UserProvider, { useUser } from './UserContext';
|
||||
import FormProvider, { useForm } from './FormContext';
|
||||
import ErrorProvider, { useError } from './ErrorContext';
|
||||
import ModeProvider, { useMode } from './ModeContext';
|
||||
|
||||
export {
|
||||
AnswerProvider,
|
||||
|
|
@ -17,4 +18,6 @@ export {
|
|||
useForm,
|
||||
ErrorProvider,
|
||||
useError,
|
||||
ModeProvider,
|
||||
useMode,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,11 +32,9 @@ export default function FormController({
|
|||
})
|
||||
);
|
||||
|
||||
return (
|
||||
Form({
|
||||
formik,
|
||||
fields: formikFields,
|
||||
children
|
||||
})
|
||||
);
|
||||
return Form({
|
||||
formik,
|
||||
fields: formikFields,
|
||||
children,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { Form } from 'controllers/FormControllers';
|
|||
import { login } from 'services/userServices';
|
||||
import { loginFields } from 'services/fields';
|
||||
import { loginSchema } from 'services/schemas';
|
||||
import { getUserLevel } from 'services/getUserLevel';
|
||||
import Cookies from 'js-cookie';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
|
@ -16,7 +17,8 @@ export default function LoginFormController() {
|
|||
if (req.error) {
|
||||
setError(req.error);
|
||||
} else {
|
||||
setUserData(() => req.user);
|
||||
const data = { ...req.user, level: getUserLevel(req.user.points) };
|
||||
setUserData(() => data);
|
||||
Cookies.set('token', req.token);
|
||||
navigate('/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ export default function RegisterFormController() {
|
|||
|
||||
const validateRegister = async ({ username, email, password }) => {
|
||||
const req = await register({ username, email, password });
|
||||
if (req.error) {
|
||||
if (req?.error) {
|
||||
setError(req.error);
|
||||
} else {
|
||||
navigate('/login');
|
||||
navigate('/users/login');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,31 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { useUser } from 'contexts';
|
||||
import { useError } from 'contexts';
|
||||
import { getMail } from 'services/mailServices';
|
||||
import { MailUnit } from 'components';
|
||||
|
||||
export default function InboxController() {
|
||||
const { userData } = useUser();
|
||||
const { setError } = useError();
|
||||
const [mail, setMail] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchMail = async () => {
|
||||
const { success, messages } = await getMail(userData?.username);
|
||||
if (success) {
|
||||
setMail(() => messages);
|
||||
}
|
||||
const fetchMail = async () => {
|
||||
const req = await getMail();
|
||||
|
||||
if (req.error) {
|
||||
setError(() => req.error);
|
||||
} else {
|
||||
setMail(() => req.messages);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchMail();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{mail.map((message) => (
|
||||
<MailUnit {...message} />
|
||||
<MailUnit {...message} key={message.id} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,24 @@
|
|||
import { useUser } from 'contexts';
|
||||
import { useUser, useError } from 'contexts';
|
||||
import { Navbar } from 'components';
|
||||
import { logout } from 'services/userServices';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
export default function NavbarController() {
|
||||
const { logout, userData } = useUser();
|
||||
const { userData, setUserData } = useUser();
|
||||
const { setError } = useError();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return Navbar({ logout, userData });
|
||||
const logoutUser = async () => {
|
||||
const request = await logout();
|
||||
if (request.error) {
|
||||
setError(() => request.error);
|
||||
} else {
|
||||
Cookies.remove('token');
|
||||
setUserData(() => ({}));
|
||||
navigate('/users/login');
|
||||
}
|
||||
};
|
||||
|
||||
return Navbar({ logout: logoutUser, userData });
|
||||
}
|
||||
|
|
|
|||
17
client/src/controllers/SearchBarController.js
Normal file
17
client/src/controllers/SearchBarController.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { useState } from 'react';
|
||||
import { SearchBar } from 'components';
|
||||
|
||||
export default function SearchBarController() {
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
const onChange = (e) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
const onSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log(search);
|
||||
};
|
||||
|
||||
return <SearchBar value={search} onChange={onChange} onSubmit={onSubmit} />;
|
||||
}
|
||||
|
|
@ -1,34 +1,45 @@
|
|||
import { useUser } from 'contexts';
|
||||
import { useUser, useError } from 'contexts';
|
||||
import { Form } from 'controllers/FormControllers';
|
||||
import { composeMailFields } from 'services/fields';
|
||||
import { postMail } from 'services/mailServices';
|
||||
import { mailSchema } from 'services/schemas';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function SendMailController() {
|
||||
|
||||
const { userData } = useUser();
|
||||
|
||||
const { setError } = useError();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if(checkAuth()){
|
||||
navigate('/users/login', {state: {name : 'ask', msg : 'you need to be authenticated to access this feature' , prevPath: '/ask'}})
|
||||
if (checkAuth()) {
|
||||
navigate('/users/login', {
|
||||
state: {
|
||||
name: 'ask',
|
||||
msg: 'you need to be authenticated to access this feature',
|
||||
prevPath: '/ask',
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
function checkAuth(){
|
||||
|
||||
if(!userData.username){
|
||||
function checkAuth() {
|
||||
if (!userData.username) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const sendMail = ({ receiver, subject, text }) =>
|
||||
postMail(userData.username, receiver, subject, text);
|
||||
const sendMail = async ({ receiver, subject, text }) => {
|
||||
const request = postMail(userData.username, receiver, subject, text);
|
||||
|
||||
if (request?.error) {
|
||||
setError(request.error);
|
||||
}
|
||||
};
|
||||
|
||||
return Form({
|
||||
fields: composeMailFields,
|
||||
onSubmit: sendMail,
|
||||
validationSchema: mailSchema
|
||||
validationSchema: mailSchema,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,6 @@ import Inbox from './InboxController';
|
|||
import Navbar from './NavbarController';
|
||||
import PaginatedList from './PaginatedListController';
|
||||
import SendMail from './SendMailController';
|
||||
import SearchBar from './SearchBarController';
|
||||
|
||||
export {
|
||||
CreationInfoTag,
|
||||
PaginatedList,
|
||||
Inbox,
|
||||
Navbar,
|
||||
SendMail
|
||||
};
|
||||
export { CreationInfoTag, PaginatedList, Inbox, Navbar, SendMail, SearchBar };
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ const passwordStrength = ({ length }) => {
|
|||
else return 'strong';
|
||||
};
|
||||
|
||||
const answerFields = [{
|
||||
id: 'answer',
|
||||
title: 'Answer'
|
||||
}]
|
||||
const answerFields = [
|
||||
{
|
||||
id: 'answer',
|
||||
title: 'Answer',
|
||||
},
|
||||
];
|
||||
|
||||
const askQuestionFields = [
|
||||
{
|
||||
|
|
@ -17,14 +19,16 @@ const askQuestionFields = [
|
|||
{
|
||||
id: 'text',
|
||||
label: 'Text',
|
||||
multiline: true
|
||||
}
|
||||
multiline: true,
|
||||
},
|
||||
];
|
||||
|
||||
const commentFields = [{
|
||||
id: 'comment',
|
||||
label: 'Comment'
|
||||
}]
|
||||
const commentFields = [
|
||||
{
|
||||
id: 'comment',
|
||||
label: 'Comment',
|
||||
},
|
||||
];
|
||||
|
||||
const composeMailFields = [
|
||||
{
|
||||
|
|
@ -38,6 +42,7 @@ const composeMailFields = [
|
|||
{
|
||||
id: 'text',
|
||||
label: 'Body',
|
||||
multiline: true,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -53,16 +58,17 @@ const loginFields = [
|
|||
},
|
||||
];
|
||||
|
||||
const recoverFields = [{
|
||||
id: 'username',
|
||||
title: 'Username'
|
||||
}];
|
||||
const recoverFields = [
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
},
|
||||
];
|
||||
|
||||
const registerFields = [
|
||||
{
|
||||
id: 'username',
|
||||
title: 'Username',
|
||||
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
|
|
|
|||
9
client/src/services/getGravatarURL.js
Normal file
9
client/src/services/getGravatarURL.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
const { MD5 } = require('crypto-js');
|
||||
|
||||
function getGravatarURL(email) {
|
||||
return `https://www.gravatar.com/avatar/${MD5(
|
||||
email.toLowerCase().trim()
|
||||
)}?d=identicon`;
|
||||
}
|
||||
|
||||
export { getGravatarURL };
|
||||
22
client/src/services/getUserLevel.js
Normal file
22
client/src/services/getUserLevel.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
function getUserLevel(points) {
|
||||
switch (points) {
|
||||
case points > 10000:
|
||||
return 7;
|
||||
case points > 3000:
|
||||
return 6;
|
||||
case points > 1000:
|
||||
return 5;
|
||||
case points > 125:
|
||||
return 4;
|
||||
case points > 50:
|
||||
return 3;
|
||||
case points > 15:
|
||||
return 2;
|
||||
case points > 0:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export { getUserLevel };
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"Level 2": 15,
|
||||
"Level 3": 50,
|
||||
"Level 4": 125,
|
||||
"Level 5": 1000,
|
||||
"Level 6": 3000,
|
||||
"Level 7": 10000
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@ import Cookies from 'js-cookie';
|
|||
|
||||
const callMailAPI = createEndpoint('/mail');
|
||||
|
||||
const postMail = async () =>
|
||||
callMailAPI('post', ``)
|
||||
const postMail = async (data) =>
|
||||
await callMailAPI('post', ``)
|
||||
.set('Authorization', `bearer ${Cookies.get('token')}`)
|
||||
.send(data)
|
||||
.then((res) => res.body)
|
||||
.catch((err) => {
|
||||
console.log(err.response.body.error);
|
||||
|
|
@ -13,7 +14,7 @@ const postMail = async () =>
|
|||
});
|
||||
|
||||
const getMail = async () =>
|
||||
callMailAPI('get')
|
||||
await callMailAPI('get', '')
|
||||
.set('Authorization', `bearer ${Cookies.get('token')}`)
|
||||
.then((res) => res.body)
|
||||
.catch((err) => {
|
||||
|
|
|
|||
|
|
@ -25,6 +25,16 @@ const login = async ({ username, password }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const logout = async () =>
|
||||
await callUsersAPI('post', `/login`)
|
||||
.set('Authorization', `bearer ${Cookies.get('token')}`)
|
||||
.send({ remember: false })
|
||||
.then((res) => res.body)
|
||||
.catch((err) => {
|
||||
console.log(err.response.body.error);
|
||||
return err.response.body;
|
||||
});
|
||||
|
||||
const getUser = async (username) =>
|
||||
callUsersAPI('get', `/${username}`)
|
||||
.then((res) => res.body)
|
||||
|
|
@ -81,6 +91,7 @@ export {
|
|||
getUserAnswers,
|
||||
getUserQuestions,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
updateUser,
|
||||
requestReset,
|
||||
|
|
|
|||
17
package-lock.json
generated
17
package-lock.json
generated
|
|
@ -11,10 +11,7 @@
|
|||
"workspaces": [
|
||||
"client",
|
||||
"server"
|
||||
],
|
||||
"dependencies": {
|
||||
"js-cookie": "^3.0.1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"client": {
|
||||
"name": "qoverflow",
|
||||
|
|
@ -38,6 +35,7 @@
|
|||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-time-ago": "^7.2.1",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"superagent": "^8.0.0",
|
||||
"superagent-throttle": "^1.0.1",
|
||||
"universal-cookie": "^4.0.4",
|
||||
|
|
@ -17720,6 +17718,11 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remove-markdown": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.0.tgz",
|
||||
"integrity": "sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg=="
|
||||
},
|
||||
"node_modules/renderkid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||
|
|
@ -32179,6 +32182,7 @@
|
|||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-time-ago": "^7.2.1",
|
||||
"remove-markdown": "*",
|
||||
"superagent": "^8.0.0",
|
||||
"superagent-throttle": "^1.0.1",
|
||||
"universal-cookie": "^4.0.4",
|
||||
|
|
@ -33543,6 +33547,11 @@
|
|||
"unified": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"remove-markdown": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.5.0.tgz",
|
||||
"integrity": "sha512-x917M80K97K5IN1L8lUvFehsfhR8cYjGQ/yAMRI9E7JIKivtl5Emo5iD13DhMr+VojzMCiYk8V2byNPwT/oapg=="
|
||||
},
|
||||
"renderkid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -24,8 +24,5 @@
|
|||
"client",
|
||||
"server"
|
||||
],
|
||||
"homepage": "https://github.com/spicecat/qOverflow#readme",
|
||||
"dependencies": {
|
||||
"js-cookie": "^3.0.1"
|
||||
}
|
||||
"homepage": "https://github.com/spicecat/qOverflow#readme"
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue