Added material-ui to RWA example.

This commit is contained in:
Matija Sosic 2021-02-05 10:57:26 +01:00
parent 80d6278798
commit 4bd68ee56b
9 changed files with 558 additions and 181 deletions

View File

@ -2,6 +2,16 @@ import React, { useState } from 'react'
import { Link } from 'react-router-dom'
import _ from 'lodash'
import Container from '@material-ui/core/Container'
import Grid from '@material-ui/core/Grid'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Box from '@material-ui/core/Box'
import Typography from '@material-ui/core/Typography'
import Chip from '@material-ui/core/Chip'
import Paper from '@material-ui/core/Paper';
import { makeStyles } from '@material-ui/core/styles'
import useAuth from '@wasp/auth/useAuth.js'
import { useQuery } from '@wasp/queries'
@ -15,35 +25,98 @@ const MainPage = () => {
const { data: me } = useAuth()
return (
<div>
<Container maxWidth="lg">
<Navbar />
<Tags />
<Grid container spacing={2}>
<Grid item xs={8}>
<FeedTabs me={me}/>
</Grid>
<Grid item xs={4}>
<Tags />
</Grid>
</Grid>
{ me && (
<div>
<h1> Your Feed </h1>
</Container>
)
}
const useStylesFeedTabs = makeStyles((theme) => ({
root: {
marginBottom: theme.spacing(2),
},
}))
const FeedTabs = ({ me }) => {
const classes = useStylesFeedTabs()
const [value, setValue] = useState(0);
const handleChange = (event, newValue) => setValue(newValue)
return (
<>
<Tabs value={value} onChange={handleChange} className={classes.root}>
<Tab label="Your Feed" id="feed-tabpanel-0"/>
<Tab label="Global Feed" id="feed-tabpanel-1"/>
</Tabs>
<TabPanel value={value} index={0}>
{ me && (
<ArticleListPaginated
query={getFollowedArticles}
makeQueryArgs={({ skip, take }) => ({ skip, take })}
pageSize={2}
pageSize={5}
/>
</div>
)}
)}
</TabPanel>
<div>
<h1> Global Feed </h1>
<TabPanel value={value} index={1}>
<ArticleListPaginated
query={getAllArticles}
makeQueryArgs={({ skip, take }) => ({ skip, take })}
pageSize={2}
pageSize={5}
/>
</div>
</TabPanel>
</>
)
}
function TabPanel(props) {
const { children, value, index, ...other } = props
return (
<div
role="tabpanel"
hidden={value !== index}
id={`feed-tabpanel-${index}`}
{...other}
>
{value === index && (
<Box>
{children}
</Box>
)}
</div>
)
}
const useStylesTags = makeStyles((theme) => ({
root: {
padding: theme.spacing(0.5),
margin: 0,
},
chip: {
margin: theme.spacing(0.5),
},
title: {
marginLeft: theme.spacing(0.5)
}
}))
const Tags = () => {
const classes = useStylesTags()
const { data: tags } = useQuery(getTags)
if (!tags) return null
@ -51,13 +124,15 @@ const Tags = () => {
const popularTags = _.take(_.sortBy(tags, [t => -1 * t.numArticles]), 10)
return (
<div>
Popular tags: { popularTags.map(tag => (
<div>
{ tag.name } ({ tag.numArticles })
</div>
))}
</div>
<Paper className={classes.root}>
<Typography variant="subtitle1" className={classes.title}>Popular tags</Typography>
{ popularTags.map(tag => (
<Chip
className={classes.chip}
label={`${tag.name} (${tag.numArticles})`}
/>
))}
</Paper>
)
}

View File

@ -1,26 +1,57 @@
import React from 'react'
import { Link } from 'react-router-dom'
import AppBar from '@material-ui/core/AppBar'
import Toolbar from '@material-ui/core/Toolbar'
import Button from '@material-ui/core/Button'
import Typography from '@material-ui/core/Typography'
import { makeStyles } from '@material-ui/core/styles'
import useAuth from '@wasp/auth/useAuth.js'
const Navbar = () => {
const { data: user } = useAuth()
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
marginBottom: 50
},
title: {
flexGrow: 1,
},
}));
const Navbar = () => {
const classes = useStyles()
const { data: user } = useAuth()
if (user) {
return (
<div>
<Link to='/'> Home </Link>
<Link to='/editor'> New Article </Link>
<Link to='/settings'> Settings </Link>
<Link to={`/@${user.username}`}> { user.username } </Link>
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>Conduit</Typography>
<Button component={ Link } to="/" color="inherit">Home</Button>
<Button component={ Link } to="/editor" color="inherit">New Article</Button>
<Button component={ Link } to="/settings" color="inherit">Settings</Button>
<Button component={ Link } to={`/@${user.username}`} color="inherit">{ user.username }</Button>
</Toolbar>
</AppBar>
</div>
)
} else {
return (
<div>
<Link to='/login'> Sign in </Link>
<Link to='/register'> Sign up </Link>
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>Conduit</Typography>
<Button component={ Link } to="/login" color="inherit">Sign in</Button>
<Button component={ Link } to="/register" color="inherit">Sign up</Button>
</Toolbar>
</AppBar>
</div>
)
}

View File

@ -2,6 +2,13 @@ import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { Link, useHistory } from 'react-router-dom'
import Container from '@material-ui/core/Container'
import TextField from '@material-ui/core/TextField'
import Grid from '@material-ui/core/Grid'
import Chip from '@material-ui/core/Chip'
import Button from '@material-ui/core/Button'
import { makeStyles } from '@material-ui/core/styles'
import logout from '@wasp/auth/logout.js'
import { useQuery } from '@wasp/queries'
@ -11,6 +18,27 @@ import getArticle from '@wasp/queries/getArticle'
import Navbar from '../../Navbar'
const useStyles = makeStyles((theme) => ({
/*
root: {
display: 'flex',
flexWrap: 'wrap',
},
*/
textField: {
//marginLeft: theme.spacing(1),
//width: '25ch',
marginBottom: theme.spacing(3)
},
tags: {
'& *:not(:last-child)': {
marginRight: theme.spacing(0.5)
},
marginBottom: theme.spacing(3)
}
}))
const ArticleEditorPage = (props) => {
// TODO: Here, as in some other places, it feels tricky to figure out what is happening regarding the state.
// When is article null, when not, should I look into combination of article and articleSlug, then
@ -21,10 +49,14 @@ const ArticleEditorPage = (props) => {
return articleError
? articleError.message || articleError
: (
<div>
<Container maxWidth="lg">
<Navbar />
<ArticleEditor user={props.user} article={article} />
</div>
<Grid container direction="row" justify="center">
<Grid item xs={8}>
<ArticleEditor user={props.user} article={article} />
</Grid>
</Grid>
</Container>
)
}
@ -32,8 +64,9 @@ ArticleEditorPage.propTypes = {
user: PropTypes.object
}
const ArticleEditor = (props) => {
const classes = useStyles()
const user = props.user
const article = props.article
@ -85,30 +118,37 @@ const ArticleEditor = (props) => {
) }
<form onSubmit={handleSubmit}>
<h2>Article title</h2>
<input
type='text'
value={title}
<TextField
className={classes.textField}
label="Article Title"
fullWidth
value={title}
onChange={e => setTitle(e.target.value)}
/>
<h2>What's this article about?</h2>
<input
type='text'
value={description}
<TextField
className={classes.textField}
label="What's this article about"
fullWidth
value={description}
onChange={e => setDescription(e.target.value)}
/>
<h2>Markdown content</h2>
<textarea
value={markdownContent}
<TextField
className={classes.textField}
label="Markdown content"
multiline
rows={3}
fullWidth
value={markdownContent}
onChange={e => setMarkdownContent(e.target.value)}
/>
<h2>Enter tags</h2>
<input
type="text"
value={newTagName}
<TextField
className={classes.textField}
label="Enter tags"
fullWidth
value={newTagName}
onChange={e => setNewTagName(e.target.value)}
onKeyPress={e => {
if (e.key === 'Enter') {
@ -118,18 +158,14 @@ const ArticleEditor = (props) => {
}
}}
/>
<div>
<div className={classes.tags}>
{ tags.map(tag => (
<div key={tag.name}>
{tag.name}
<button onClick={() => setTags(tags.filter(t => t !== tag))}> X </button>
</div>
<Chip label={tag.name} onDelete={() => setTags(tags.filter(t => t !== tag))}/>
))}
</div>
<div>
<input type='submit' value='Publish Article' />
</div>
<Button type='submit' color='primary' variant='contained'>Publish Article</Button>
</form>
</div>
)

View File

@ -2,12 +2,40 @@ import React from 'react'
import { Link } from 'react-router-dom'
import moment from 'moment'
import Button from '@material-ui/core/Button'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import CardActions from '@material-ui/core/CardActions'
import CardHeader from '@material-ui/core/CardHeader'
import Avatar from '@material-ui/core/Avatar'
import Typography from '@material-ui/core/Typography'
import IconButton from '@material-ui/core/IconButton'
import FavoriteIcon from '@material-ui/icons/Favorite'
import { makeStyles } from '@material-ui/core/styles'
import Chip from '@material-ui/core/Chip'
import setArticleFavorited from '@wasp/actions/setArticleFavorited'
import smileyImageUrl from '../../smiley.jpg'
const useStyles = makeStyles((theme) => ({
root: {
},
article: {
marginBottom: theme.spacing(1)
},
tags: {
marginLeft: 'auto'
},
chip: {
margin: theme.spacing(0.5),
},
}));
const ArticleList = (props) => {
const articles = props.articles
return articles ? (
<div>
{ articles.map(article => <Article article={article} key={article.id} />) }
@ -16,6 +44,7 @@ const ArticleList = (props) => {
}
const Article = (props) => {
const classes = useStyles()
const article = props.article
const toggleArticleFavorited = async () => {
@ -23,26 +52,42 @@ const Article = (props) => {
}
return (
<div style={{ border: '1px solid black' }}>
<Link to={`/article/${article.slug}`}>
<h2> { article.title } </h2>
</Link>
<p> { article.description } </p>
<p>
<em> Tags: </em>
{ article.tags.map(t => t.name).join('; ') }
</p>
<p>
<img src={ article.user.profilePictureUrl || smileyImageUrl } width='30px' />
<div> { article.user.username } </div>
<div> { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
</p>
<div>
<button onClick={toggleArticleFavorited}>
<Card className={classes.article} elevation={2}>
<CardHeader
avatar={<Avatar>A</Avatar>}
title={article.user.username}
subheader={moment(article.createdAt).format('MMMM DD, YYYY')}
/>
<CardContent>
<Typography variant="h5">
<Link to={`/article/${article.slug}`}>
{ article.title }
</Link>
</Typography>
<Typography variant="body2" color="textSecondary" component="p">
{ article.description }
</Typography>
</CardContent>
<CardActions disableSpacing>
<Button onClick={toggleArticleFavorited} size="small" color="primary">
{ article.favorited ? 'Unlike' : 'Like' } ({ article.favoritesCount })
</button>
</div>
</div>
</Button>
<Button size="small" color="primary">
Read more
</Button>
<span className={classes.tags}>
{ article.tags.map(t => (
<Chip className={classes.chip} label={t.name}/>
))}
</span>
</CardActions>
</Card>
)
}

View File

@ -4,6 +4,19 @@ import ReactMarkdown from 'react-markdown'
import moment from 'moment'
import PropTypes from 'prop-types'
import Container from '@material-ui/core/Container'
import Grid from '@material-ui/core/Grid'
import Card from '@material-ui/core/Card'
import CardContent from '@material-ui/core/CardContent'
import CardActions from '@material-ui/core/CardActions'
import CardHeader from '@material-ui/core/CardHeader'
import TextField from '@material-ui/core/TextField'
import Typography from '@material-ui/core/Typography'
import Avatar from '@material-ui/core/Avatar'
import Chip from '@material-ui/core/Chip'
import Button from '@material-ui/core/Button'
import { makeStyles } from '@material-ui/core/styles'
import useAuth from '@wasp/auth/useAuth.js'
import { useQuery } from '@wasp/queries'
@ -16,7 +29,35 @@ import deleteComment from '@wasp/actions/deleteComment'
import Navbar from '../../Navbar'
const useStyles = makeStyles((theme) => ({
tags: {
'& *:not(:last-child)': {
marginRight: theme.spacing(0.5)
},
marginBottom: theme.spacing(3)
},
comments: {
'& *:not(:last-child)': {
marginBottom: theme.spacing(0.5)
},
},
ownArticleButtons: {
'& *:not(:last-child)': {
marginRight: theme.spacing(0.5)
},
marginBottom: theme.spacing(3)
},
textField: {
marginBottom: theme.spacing(3)
},
postCommentButton: {
marginBottom: theme.spacing(3)
}
}))
const ArticleViewPage = (props) => {
const classes = useStyles()
const history = useHistory()
const { data: me } = useAuth({ keepPreviousData: true })
@ -50,59 +91,77 @@ const ArticleViewPage = (props) => {
}
return article ? (
<div>
<Container maxWidth="lg">
<Navbar />
<div>
<div> Author: { article.user.username } </div>
<div> Created at: { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
</div>
<Grid container direction="row" justify="center">
<Grid item xs={8}>
<Typography variant="h2">{ article.title }</Typography>
<div>
<p> { article.title } </p>
<p> { article.description } </p>
<p>
<ReactMarkdown children={article.markdownContent} />
</p>
<p>
Tags: { article.tags.map(tag => <div> {tag.name} </div>) }
</p>
</div>
<div>
<div> Author: { article.user.username } </div>
<div> Created at: { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
</div>
</Grid>
{ isMyArticle && (
<div>
<button onClick={handleEditArticle}> Edit Article </button>
<button onClick={handleDeleteArticle}> Delete Article </button>
</div>
)}
<Grid item xs={8}>
<p>
<Typography variant="h5">
<ReactMarkdown children={article.markdownContent} />
</Typography>
</p>
</Grid>
<Comments article={article}/>
</div>
<Grid item xs={8}>
<div className={classes.tags}>
<span>Tags:</span>
{ article.tags.map(tag => <Chip label={tag.name} />) }
</div>
{ isMyArticle && (
<div className={classes.ownArticleButtons}>
<Button color="primary" variant="outlined" onClick={handleEditArticle}>
Edit Article
</Button>
<Button color="secondary" variant="outlined" onClick={handleDeleteArticle}>
Delete Article
</Button>
</div>
)}
<Comments article={article}/>
</Grid>
</Grid>
</Container>
) : null
}
const Comments = (props) => {
const classes = useStyles()
const article = props.article
const { data: me } = useAuth()
const { data: comments } = useQuery(getArticleComments, { articleId: article.id })
return comments ? (
return (
<div>
{ me
? <CreateComment article={article} />
: null // TODO: Instead of nothing, tell them they need to sign up / sign in to comment.
}
<div>
{ comments.length
? comments.map(c => <Comment comment={c} key={c.id} />)
: 'No comments yet!'
}
</div>
{ comments ? (
<div className={classes.comments}>
{ comments.length
? comments.map(c => <Comment comment={c} key={c.id} />)
: 'No comments yet!'
}
</div>
) : null }
</div>
) : null
)
}
Comments.propTypes = {
article: PropTypes.object.isRequired
@ -123,17 +182,28 @@ const Comment = (props) => {
}
return (
<div style={{ border: '1px solid black', width: '300px' }}>
<div> { comment.content } </div>
<div> { moment(comment.createdAt).format('MMMM DD, YYYY') } </div>
{ /* TODO: Show user's profile picture. */ }
{ /* TODO: Make username a link to the user profile. */ }
<div> { comment.user.username } </div>
{ (me && me.id === comment.userId)
? <button onClick={onDelete}> Delete </button>
: null
}
</div>
<>
<Card>
<CardHeader
avatar={<Avatar>R</Avatar>}
title={comment.user.username}
subheader={ moment(comment.createdAt).format('MMMM DD, YYYY') }
/>
<CardContent>
<Typography variant="body1">
{ comment.content }
</Typography>
</CardContent>
<CardActions>
{ (me && me.id === comment.userId)
? <Button size="small" color="primary" onClick={onDelete}>Delete</Button>
: null
}
</CardActions>
</Card>
</>
)
}
Comment.propTypes = {
@ -141,6 +211,8 @@ Comment.propTypes = {
}
const CreateComment = (props) => {
const classes = useStyles()
const article = props.article
const [content, setContent] = useState('')
@ -157,13 +229,17 @@ const CreateComment = (props) => {
return (
<form onSubmit={handleSubmit}>
<textarea
<TextField
className={classes.textField}
label='Leave a comment'
multiline
fullWidth
rows={3}
value={content}
onChange={e => setContent(e.target.value)}
style={{ width: '300px' }}
/>
<div>
<input type='submit' value='Post Comment' />
<div className={classes.postCommentButton}>
<Button type="submit" color="primary" variant="contained">Post Comment</Button>
</div>
</form>
)

View File

@ -88,10 +88,10 @@ export const getArticle = async ({ slug }, context) => {
return article
}
export const getArticleComments = async ({ slug }, context) => {
export const getArticleComments = async ({ articleId }, context) => {
// TODO: Do some error handling?
const comments = await context.entities.Comment.findMany({
where: { article: { slug } },
where: { articleId },
include: {
user: {
// TODO: Tricky, if you forget this you could return unwanted fields

View File

@ -1,6 +1,15 @@
import React, { useState } from 'react'
import { Link, useHistory } from 'react-router-dom'
import Container from '@material-ui/core/Container'
import TextField from '@material-ui/core/TextField'
import Grid from '@material-ui/core/Grid'
import Tabs from '@material-ui/core/Tabs'
import Tab from '@material-ui/core/Tab'
import Box from '@material-ui/core/Box'
import Button from '@material-ui/core/Button'
import { makeStyles } from '@material-ui/core/styles'
import useAuth from '@wasp/auth/useAuth.js'
import { useQuery } from '@wasp/queries'
@ -12,7 +21,16 @@ import Navbar from '../../Navbar'
import ArticleListPaginated from '../../article/components/ArticleListPaginated'
import smileyImageUrl from '../../smiley.jpg'
const useStyles = makeStyles((theme) => ({
articles: {
marginTop: theme.spacing(5)
}
}))
const UserProfilePage = (props) => {
const classes = useStyles()
const history = useHistory()
const { data: me } = useAuth()
@ -30,25 +48,32 @@ const UserProfilePage = (props) => {
}
return user ? (
<div>
<Container maxWidth="lg">
<Navbar />
<img src={user.profilePictureUrl || smileyImageUrl} />
<p> { user.username } </p>
<p> { user.bio } </p>
{ me && me.username === username && (
<div>
<Link to='/settings'>Edit Profile Settings</Link>
</div>
)}
{ me && me.username !== username && (
<div>
<FollowUserButton user={user} />
</div>
)}
<Grid container direction="row" justify="center">
<Grid item xs={8}>
<img src={user.profilePictureUrl || smileyImageUrl} />
<p> { user.username } </p>
<p> { user.bio } </p>
{ me && me.username === username && (
<div>
<Button component={ Link } to="/settings" variant="contained" color="primary">
Edit Profile Settings
</Button>
</div>
)}
{ me && me.username !== username && (
<div>
<FollowUserButton user={user} />
</div>
)}
<Articles user={user} />
</div>
<Articles user={user} />
</Grid>
</Grid>
</Container>
) : null
}
@ -72,23 +97,72 @@ const FollowUserButton = (props) => {
) : null
}
const useStylesFeedTabs = makeStyles((theme) => ({
root: {
marginBottom: theme.spacing(2),
},
}))
const ProfileFeedTabs = (props) => {
const classes = useStylesFeedTabs()
const [value, setValue] = useState(0);
const handleChange = (event, newValue) => setValue(newValue)
return (
<>
<Tabs value={value} onChange={handleChange} className={classes.root}>
<Tab label="My Articles" id="feed-tabpanel-0"/>
<Tab label="Favorited articles" id="feed-tabpanel-1"/>
</Tabs>
<TabPanel value={value} index={0}>
<ArticleListPaginated
query={getArticlesByUser}
makeQueryArgs={({ skip, take }) => ({ username: props.user.username, skip, take })}
pageSize={5}
/>
</TabPanel>
<TabPanel value={value} index={1}>
<ArticleListPaginated
query={getFavoritedArticles}
makeQueryArgs={({ skip, take }) => ({ username: props.user.username, skip, take })}
pageSize={5}
/>
</TabPanel>
</>
)
}
function TabPanel(props) {
const { children, value, index, ...other } = props
return (
<div
role="tabpanel"
hidden={value !== index}
id={`feed-tabpanel-${index}`}
{...other}
>
{value === index && (
<Box>
{children}
</Box>
)}
</div>
)
}
const Articles = (props) => {
const classes = useStyles()
const user = props.user
return (
<div>
<h1> My Articles </h1>
<ArticleListPaginated
query={getArticlesByUser}
makeQueryArgs={({ skip, take }) => ({ username: props.user.username, skip, take })}
pageSize={2}
/>
<h1> Favorited Articles </h1>
<ArticleListPaginated
query={getFavoritedArticles}
makeQueryArgs={({ skip, take }) => ({ username: props.user.username, skip, take })}
pageSize={2}
/>
<div className={classes.articles}>
<ProfileFeedTabs {...props} />
</div>
)
}

View File

@ -1,6 +1,12 @@
import React, { useState } from 'react'
import { Link, useHistory } from 'react-router-dom'
import Container from '@material-ui/core/Container'
import Grid from '@material-ui/core/Grid'
import TextField from '@material-ui/core/TextField'
import Button from '@material-ui/core/Button'
import { makeStyles } from '@material-ui/core/styles'
import logout from '@wasp/auth/logout.js'
import updateUser from '@wasp/actions/updateUser'
@ -9,16 +15,31 @@ import Navbar from '../../Navbar'
const UserSettingsPage = ({ user }) => {
return (
<div>
<Container maxWidth="lg">
<Navbar />
<UserSettings user={user}/>
</div>
<Grid container direction="row" justify="center">
<Grid item xs={6}>
<UserSettings user={user}/>
</Grid>
</Grid>
</Container>
)
}
const useStyles = makeStyles((theme) => ({
textField: {
//width: '25ch',
marginBottom: theme.spacing(3)
},
logoutButton: {
marginTop: theme.spacing(3)
}
}))
const UserSettings = (props) => {
const classes = useStyles()
const user = props.user
const history = useHistory()
@ -64,46 +85,59 @@ const UserSettings = (props) => {
<form onSubmit={handleSubmit}>
<h2>URL of profile picture</h2>
<input
type='text'
value={profilePictureUrl}
<TextField
className={classes.textField}
label="URL of profile picture"
fullWidth
value={profilePictureUrl}
onChange={e => setProfilePictureUrl(e.target.value)}
/>
<h2>Username</h2>
<input
type='text'
value={username}
<TextField
className={classes.textField}
label="Username"
fullWidth
value={username}
onChange={e => setUsername(e.target.value)}
/>
<h2>Short bio</h2>
<textarea
value={bio}
<TextField
className={classes.textField}
label="Short bio"
multiline
rows={3}
fullWidth
value={bio}
onChange={e => setBio(e.target.value)}
/>
<h2>Email</h2>
<input
type='text'
value={email}
<TextField
className={classes.textField}
label="Email"
fullWidth
value={email}
onChange={e => setEmail(e.target.value)}
/>
<h2>New password</h2>
<input
type='password'
value={newPassword}
<TextField
className={classes.textField}
label="New password"
type="password"
fullWidth
value={newPassword}
onChange={e => setNewPassword(e.target.value)}
/>
<div>
<input type='submit' value='Update Settings' />
</div>
<Button type="submit" color="primary" variant="contained">Update Settings</Button>
</form>
<button onClick={handleLogout}> Log out </button>
<Button
className={classes.logoutButton}
type="submit" color="secondary"
variant="contained" onClick={handleLogout}
>
Log out
</Button>
</div>
)
}

View File

@ -1,5 +1,9 @@
app Conduit {
title: "Conduit"
title: "Conduit",
head: [
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
]
}
auth {
@ -187,5 +191,7 @@ dependencies {=json
"prop-types": "15.7.2",
"react-markdown": "5.0.3",
"moment": "2.29.1",
"@material-ui/core": "4.11.3",
"@material-ui/icons": "4.11.2",
"slug": "4.0.2"
json=}