fix tooltip null title

This commit is contained in:
“spicecat” 2022-08-02 14:15:17 -05:00
commit b350a419c7
15 changed files with 393 additions and 165 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 C4 Patino, Andy Teh, Tejus Krishan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,157 @@
import {
AppBar,
Button,
IconButton,
Toolbar,
Typography,
Box,
Drawer,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
} from '@mui/material';
import MailIcon from '@mui/icons-material/Mail';
import LoginIcon from '@mui/icons-material/Login';
import LogoutIcon from '@mui/icons-material/Logout';
import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import SearchIcon from '@mui/icons-material/Search';
import { Link } from 'react-router-dom';
import Logo from 'assets/bdpa-logo.svg';
import { useState } from 'react';
import Gravatar from 'react-gravatar';
import { useMode } from 'contexts';
export default function MobileNavbar({ logout, userData }) {
const [open, setOpen] = useState(false);
const { mode, setMode } = useMode();
const handleClick = () => setOpen((initial) => !initial);
const onClick = () => {
setMode(() => (mode === 'light' ? 'dark' : 'light'));
};
function NavbarControls() {
return userData.username ? (
<>
<ListItem>
<ListItemButton
color='inherit'
component={Link}
to='/dashboard'
>
<ListItemIcon>
<Gravatar
email={userData.email}
size={20}
style={{ borderRadius: '100%' }}
/>
</ListItemIcon>
<ListItemText>Dashboard</ListItemText>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton color='inherit' component={Link} to='/mail'>
<ListItemIcon>
<MailIcon />
</ListItemIcon>
<ListItemText>Mail</ListItemText>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton
color='inherit'
component={Link}
to='/users/login'
onClick={logout}
>
<ListItemIcon>
<LogoutIcon />
</ListItemIcon>
<ListItemText>Logout</ListItemText>
</ListItemButton>
</ListItem>
</>
) : (
<>
<ListItem>
<ListItemButton
color='inherit'
component={Link}
to='/users/login'
>
<ListItemIcon>
<LoginIcon />
</ListItemIcon>
<ListItemText>Login</ListItemText>
</ListItemButton>
</ListItem>
<ListItem>
<ListItemButton
color='inherit'
component={Link}
to='/users/register'
>
<ListItemIcon>
<AppRegistrationIcon />
</ListItemIcon>
<ListItemText>Register</ListItemText>
</ListItemButton>
</ListItem>
</>
);
}
return (
<AppBar position='static'>
<Toolbar>
<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'>
qOverflow
</Typography>
<Box sx={{ flexGrow: 1 }} />
<Button color='inherit' onClick={handleClick}>
Menu
</Button>
</Toolbar>
<Drawer anchor='right' open={open} onClose={handleClick}>
<List>
<ListItem>
<ListItemButton component={Link} to='/questions/search'>
<ListItemIcon>
<SearchIcon />
</ListItemIcon>
<ListItemText>Search</ListItemText>
</ListItemButton>
</ListItem>
<NavbarControls />
<ListItem>
<ListItemButton onClick={onClick}>
<ListItemIcon>
{mode === 'light' ? (
<DarkModeIcon />
) : (
<LightModeIcon />
)}
</ListItemIcon>
<ListItemText>
Switch to {mode === 'light' ? 'dark' : 'light'}{' '}
mode
</ListItemText>
</ListItemButton>
</ListItem>
</List>
</Drawer>
</AppBar>
);
}

View file

@ -1,4 +1,4 @@
import { List, Pagination } from '@mui/material';
import { List, Pagination, Box } from '@mui/material';
import { NoData } from 'components';
@ -8,22 +8,29 @@ export default function PaginatedList({
data,
handleChangePage,
noData = true,
page
page,
}) {
return (
<List sx={{ pl: 2, pr: 2, width: '100%' }}>
{data.length
? data.map((item) => Component(item))
: noData && <NoData />}
{count > 1 && (
<Pagination
count={count}
onChange={handleChangePage}
page={page}
/>
<Box
fullWidth
display='flex'
justifyContent='center'
sx={{ margin: '1vh 0' }}
>
<Pagination
count={count}
onChange={handleChangePage}
page={page}
siblingCount={1}
size='small'
/>
</Box>
)}
{
data.length
? data.map(item => Component(item))
: noData && <NoData />
}
</List>
);
}

View file

@ -8,7 +8,11 @@ import {
CommentControl,
VoteControl,
} from 'controllers/QAControllers';
import { getAnswerVote, updateAnswerVote } from 'services/questionsServices';
import {
getAnswerVote,
updateAnswerVote,
postAnswerComment,
} from 'services/questionsServices';
export default function Answer({
accepted,
@ -21,11 +25,16 @@ export default function Answer({
text,
upvotes,
}) {
const { userData: { level, username } } = useUser();
const {
userData: { level, username },
} = useUser();
const getVote = () => getAnswerVote(question_id, answer_id);
const updateVote = (data) => updateAnswerVote(question_id, answer_id, data);
const canComment = level >= 3 || creator === username;
const postComment = (data) =>
postAnswerComment(question_id, answer_id, data);
return (
<span key={answer_id}>
<ListItem disablePadding>
@ -41,10 +50,7 @@ export default function Answer({
/>
{accepted && (
<div style={{ textAlign: 'center' }}>
<Tooltip
title='Accepted Answer'
placement='right'
>
<Tooltip title='Accepted Answer' placement='right'>
<CheckIcon color='success' />
</Tooltip>
</div>
@ -55,7 +61,7 @@ export default function Answer({
{...{ createdAt, creator, text: 'answered' }}
/>
<ReactMarkdown>{text}</ReactMarkdown>
<CommentControl {...{ canComment }} />
<CommentControl {...{ canComment, postComment }} />
</ListItemText>
</ListItem>
<ListItem sx={{ pl: 8 }}>

View file

@ -6,19 +6,24 @@ import { AnswerForm } from 'controllers/FormControllers'
import MdPreview from 'components/MdPreview'
export default function CreateAnswer({ canAnswer, question_id }) {
const [show, setShow] = useState(false)
const [show, setShow] = useState(false);
function toggleShow() {
setShow(!show)
setShow(!show);
}
return (
<div>
<Tooltip title={canAnswer ? '' : 'You need to be authenticated to answer or level 5 if the question is protected'}>
<span>
<Button disabled={!canAnswer} variant="contained" onClick={toggleShow}>Add Answer</Button>
</span>
</Tooltip>
<Tooltip title={canAnswer ? '' : 'You need to be authenticated to answer or level 5 if the question is protected'}>
<span>
<Button
disabled={!canAnswer}
variant='contained'
onClick={toggleShow}
>
Add Answer
</Button>
</span>
</Tooltip>
{show && (
<Card>
<CardContent>
@ -27,9 +32,6 @@ export default function CreateAnswer({ canAnswer, question_id }) {
</CardContent>
</Card>
)}
</div>
)
);
}

View file

@ -109,24 +109,17 @@ export default function Question({
<>
<Box m={2}>
<Typography variant='h4'>{title}</Typography>
<Typography display='inline' m={1}>
Views: {views}
</Typography>
<Typography display='inline' m={1}>
Answers: {answers}{' '}
</Typography>
<Typography display='inline' m={1}>
Comments: {comments}{' '}
</Typography>
<Typography display='inline' m={1}>
{' '}
Status:
</Typography>
<Typography display='inline' m={1}>Views: {views}</Typography>
<Typography display='inline' m={1}>Answers: {answers}</Typography>
<Typography display='inline' m={1}>Comments: {comments}</Typography>
<Typography display='inline' m={1}>Status:</Typography>
<Chip
color={statusColor(status)}
label={status}
size='small'
/>
<Chip color={statusColor(status)} label={status} size='small' />
<Typography display='inline' m={1}>
Accepted Answer:
</Typography>
<Typography display='inline' m={1}>Accepted Answer:</Typography>
<Chip
color={hasAcceptedAnswer ? 'success' : 'error'}
label={hasAcceptedAnswer ? 'yes' : 'no'}
@ -134,15 +127,14 @@ export default function Question({
/>
<Button
component={Link}
to='../ask'
style={{ marginLeft: '10px' }}
display='inline'
m={1}
style={{ marginLeft: '10px' }}
to='../ask'
variant='contained'
>
Ask question
</Button>
<Tooltip title={close ? '' : 'You must be level 7'}>
<span>
<Button
@ -150,20 +142,14 @@ export default function Question({
style={{ marginLeft: '10px' }}
display='inline'
m={1}
variant='contained'
onClick={changeProtect}
variant='contained'
>
Close/Open
</Button>
</span>
</Tooltip>
<Tooltip
title={
!protect &&
'You must be level 6 and this question must be open'
}
>
<Tooltip title={protect ? '' : 'You must be level 6 and this question must be open'}>
<span>
<Button
disabled={!protect}

View file

@ -10,6 +10,7 @@ import PaginatedList from './PaginatedList';
import SearchBar from './SearchBar';
import MdPreview from './MdPreview';
import Profile from './Profile';
import MobileNavbar from './MobileNavbar';
export {
CreationInfoTag,
@ -23,5 +24,6 @@ export {
PaginatedList,
SearchBar,
MdPreview,
Profile
Profile,
MobileNavbar,
};

View file

@ -21,13 +21,9 @@ export default function Dashboard() {
const navigate = useNavigate();
const [current, setCurrent] = useState('questions');
const { userData: {
email,
level,
loading,
points,
username,
} } = useUser();
const {
userData: { email, level, loading, points, username },
} = useUser();
const onChange = (_, value) => {
setCurrent(value);
@ -35,14 +31,14 @@ export default function Dashboard() {
const getAnswers = async ({ answer_id }) => {
const { error, answers } = await getUserAnswers({ after: answer_id });
if (!error)
return answers;
if (!error) return answers;
};
const getQuestions = async ({ question_id }) => {
const { error, questions } = await getUserQuestions({ after: question_id });
if (!error)
return questions;
const { error, questions } = await getUserQuestions({
after: question_id,
});
if (!error) return questions;
};
useEffect(() => {
@ -57,62 +53,102 @@ export default function Dashboard() {
}
}, [loading, navigate]);
return username && (
<>
<Helmet>
<title>Dashboard</title>
</Helmet>
<Paper sx={{ margin: '1vh', padding: '5vh' }}>
<Grid container spacing={2}>
<Grid item md={2}>
{email && <Gravatar email={email} size={200} style={{ borderRadius: '100%' }} />}
</Grid>
<Grid item md={10}>
<Typography>Username: {username}</Typography>
<Typography>Email: {email}</Typography>
<Typography>Level: {level}</Typography>
<Typography>Points: {points}</Typography>
</Grid>
<Grid item md={12}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}
>
<Button
color='primary'
component={Link}
to='/dashboard/update'
sx={{ margin: '0 1vh' }}
variant='outlined'
fullWidth
return (
username && (
<>
<Helmet>
<title>Dashboard</title>
</Helmet>
<Paper
sx={{
margin: { xs: '0.3vh', sm: '1vh', md: '1vh' },
padding: { xs: '1vh', sm: '3vh', md: '5vh' },
}}
>
<Grid
container
spacing={2}
sx={{
display: { xs: 'flex', md: 'block' },
justifyContent: 'center',
}}
>
<Grid item md={2}>
{email && (
<Gravatar
email={email}
size={200}
style={{ borderRadius: '100%' }}
/>
)}
</Grid>
<Grid item md={10}>
<Typography>Username: {username}</Typography>
<Typography>Email: {email}</Typography>
<Typography>Level: {level}</Typography>
<Typography>Points: {points}</Typography>
</Grid>
<Grid item md={12}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}
>
Update User Information
</Button>
<ToggleButtonGroup
value={current}
exclusive
onChange={onChange}
fullWidth
>
<ToggleButton value='questions' variant='outlined'>
Questions
</ToggleButton>
<ToggleButton value='answers' variant='outlined'>
Answers
</ToggleButton>
</ToggleButtonGroup>
</Box>
</Grid>
<Button
color='primary'
component={Link}
to='/dashboard/update'
sx={{ margin: '0 1vh' }}
variant='outlined'
fullWidth
>
Update User Information
</Button>
<ToggleButtonGroup
value={current}
exclusive
onChange={onChange}
fullWidth
>
<ToggleButton
value='questions'
variant='outlined'
>
Questions
</ToggleButton>
<ToggleButton
value='answers'
variant='outlined'
>
Answers
</ToggleButton>
</ToggleButtonGroup>
</Box>
</Grid>
<Grid item md={12}>
{current === 'questions' && <PaginatedList {...{ Component: ListQuestion, getData: getQuestions }} />}
{current === 'answers' && <PaginatedList {...{ Component: ListAnswer, getData: getAnswers }} />}
<Grid item md={12}>
{current === 'questions' && (
<PaginatedList
{...{
Component: ListQuestion,
getData: getQuestions,
}}
/>
)}
{current === 'answers' && (
<PaginatedList
{...{
Component: ListAnswer,
getData: getAnswers,
}}
/>
)}
</Grid>
</Grid>
</Grid>
</Paper>
</>
</Paper>
</>
)
);
}

View file

@ -11,15 +11,15 @@ export default function ForgotPassword() {
justifyContent='center'
style={{ height: '95vh' }}
>
<Grid item xs={3.5} />
<Grid item xs={5}>
<Grid item xs />
<Grid item xs={11} sm={7} md={5}>
<Card sx={{ padding: '1vh' }}>
<CardContent>
<ForgotPasswordForm />
</CardContent>
</Card>
</Grid>
<Grid item xs={3.5} />
<Grid item xs />
</Grid>
);
}

View file

@ -23,14 +23,16 @@ export default function Login() {
justifyContent='center'
style={{ height: '92vh' }}
>
<Grid item xs={3.5} />
<Grid item xs={5}>
<Grid item xs />
<Grid item xs={11} sm={7} md={5}>
<Card sx={{ padding: '1vh' }}>
<CardContent>
<LoginForm />
<Typography variant='body1'>
Forgot your password?{' '}
<Link to='/users/recover'>Recover Password</Link>
<Link to='/users/recover'>
Recover Password
</Link>
</Typography>
</CardContent>
</Card>
@ -38,7 +40,7 @@ export default function Login() {
<Alert severity='warning'> {returnMsg()} </Alert>
)}
</Grid>
<Grid item xs={3.5} />
<Grid item xs />
</Grid>
</>
);

View file

@ -7,29 +7,29 @@ import { RegisterForm } from 'controllers/FormControllers';
export default function Register() {
return (
<>
<Helmet>
<Helmet>
<title>Register - qOverflow</title>
</Helmet>
<Grid
container
spacing={2}
alignItems='center'
justifyContent='center'
style={{ height: '92vh' }}
>
<Grid item xs={3.5} />
<Grid item xs={5}>
<Card sx={{ padding: '1vh' }}>
<CardContent>
<RegisterForm />
<div>
<img src={CAPTCHA} alt='CAPTCHA' />
</div>
</CardContent>
</Card>
<Grid
container
spacing={2}
alignItems='center'
justifyContent='center'
style={{ height: '92vh' }}
>
<Grid item xs />
<Grid item xs={11} sm={7} md={5}>
<Card sx={{ padding: '1vh' }}>
<CardContent>
<RegisterForm />
<div>
<img src={CAPTCHA} alt='CAPTCHA' />
</div>
</CardContent>
</Card>
</Grid>
<Grid item xs />
</Grid>
<Grid item xs={3.5} />
</Grid>
</>
);
}

View file

@ -28,15 +28,15 @@ export default function Register() {
justifyContent='center'
style={{ height: '92vh' }}
>
<Grid item xs={3.5} />
<Grid item xs={5}>
<Grid item xs />
<Grid item xs={11} sm={7} md={5}>
<Card sx={{ padding: '1vh' }}>
<CardContent>
<UpdateForm />
</CardContent>
</Card>
</Grid>
<Grid item xs={3.5} />
<Grid item xs />
</Grid>
);
}

View file

@ -1,7 +1,8 @@
import { useUser, useError } from 'contexts';
import { Navbar } from 'components';
import { Navbar, MobileNavbar } from 'components';
import { logout } from 'services/userServices';
import { useNavigate } from 'react-router-dom';
import { useMediaQuery } from '@mui/material';
import Cookies from 'js-cookie';
export default function NavbarController() {
@ -9,6 +10,10 @@ export default function NavbarController() {
const { setError } = useError();
const navigate = useNavigate();
const xs = useMediaQuery((theme) => theme.breakpoints.only('xs'));
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
const logoutUser = async () => {
const { error } = await logout();
if (error) {
@ -19,5 +24,7 @@ export default function NavbarController() {
navigate('/users/login');
};
return Navbar({ logout: logoutUser, userData });
return md
? Navbar({ logout: logoutUser, userData })
: MobileNavbar({ logout: logoutUser, userData });
}

View file

@ -63,7 +63,7 @@ const updateAnswerVote = async (question_id, answer_id, data) =>
const getAnswerComments = async (question_id, answer_id) =>
callQuestionsAPI('get', `/${question_id}/answers/${answer_id}/comments`);
const postAnswerComments = async (question_id, answer_id, data) =>
const postAnswerComment = async (question_id, answer_id, data) =>
callQuestionsAPI(
'post',
`/${question_id}/answers/${answer_id}/comments`,
@ -109,7 +109,7 @@ export {
getQuestion,
getQuestionComments,
postAnswer,
postAnswerComments,
postAnswerComment,
postQuestion,
postQuestionComment,
searchQuestions,

View file

@ -6,7 +6,7 @@ const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
async function CreateAnswerComment(req, res) {
const { username } = req.user;
const user = req.user;
const { question_id, answer_id } = req.params;
const { text } = req.body;
@ -20,20 +20,22 @@ async function CreateAnswerComment(req, res) {
return res.status(403).send(config.errorForbidden);
// Verify user has permissions to comment
if (getUserLevel(user.points) < 3 && cachedAnswer?.creator !== username) {
if (
getUserLevel(user.points) < 3 &&
cachedAnswer?.creator !== user.username
) {
return res.status(403).send(config.errorForbidden);
}
// Verify text is included in request body
if (!text)
return res.status(400).send({ success: false });
if (!text) return res.status(400).send({ success: false });
// Post question with BDPA server
const { success, comment } = await createRequest(
'post',
`/questions/${question_id}/answers/${answer_id}/comments`,
{
creator: username,
creator: user.username,
text,
}
);