Merge branch 'dev' into main

This commit is contained in:
Ceferino Patino 2022-07-20 21:56:14 -05:00
commit 273792eb29
34 changed files with 378 additions and 194 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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} &nbsp; </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>

View file

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

View 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>
</>
);
}

View file

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

View file

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

View file

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

View file

@ -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: {

View file

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

View file

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

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

View file

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

View file

@ -32,11 +32,9 @@ export default function FormController({
})
);
return (
Form({
formik,
fields: formikFields,
children
})
);
return Form({
formik,
fields: formikFields,
children,
});
}

View file

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

View file

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

View file

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

View file

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

View 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} />;
}

View file

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

View file

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

View file

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

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

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

View file

@ -1,8 +0,0 @@
{
"Level 2": 15,
"Level 3": 50,
"Level 4": 125,
"Level 5": 1000,
"Level 6": 3000,
"Level 7": 10000
}

View file

@ -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) => {

View file

@ -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
View file

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

View file

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