mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-25 10:03:07 +03:00
Added material-ui to RWA example.
This commit is contained in:
parent
80d6278798
commit
4bd68ee56b
@ -2,6 +2,16 @@ import React, { useState } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import _ from 'lodash'
|
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 useAuth from '@wasp/auth/useAuth.js'
|
||||||
import { useQuery } from '@wasp/queries'
|
import { useQuery } from '@wasp/queries'
|
||||||
|
|
||||||
@ -15,35 +25,98 @@ const MainPage = () => {
|
|||||||
const { data: me } = useAuth()
|
const { data: me } = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Container maxWidth="lg">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<Tags />
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={8}>
|
||||||
|
<FeedTabs me={me}/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={4}>
|
||||||
|
<Tags />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
{ me && (
|
</Container>
|
||||||
<div>
|
)
|
||||||
<h1> Your Feed </h1>
|
}
|
||||||
|
|
||||||
|
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
|
<ArticleListPaginated
|
||||||
query={getFollowedArticles}
|
query={getFollowedArticles}
|
||||||
makeQueryArgs={({ skip, take }) => ({ skip, take })}
|
makeQueryArgs={({ skip, take }) => ({ skip, take })}
|
||||||
pageSize={2}
|
pageSize={5}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</TabPanel>
|
||||||
|
|
||||||
<div>
|
<TabPanel value={value} index={1}>
|
||||||
<h1> Global Feed </h1>
|
|
||||||
<ArticleListPaginated
|
<ArticleListPaginated
|
||||||
query={getAllArticles}
|
query={getAllArticles}
|
||||||
makeQueryArgs={({ skip, take }) => ({ skip, take })}
|
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>
|
</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 Tags = () => {
|
||||||
|
const classes = useStylesTags()
|
||||||
|
|
||||||
const { data: tags } = useQuery(getTags)
|
const { data: tags } = useQuery(getTags)
|
||||||
|
|
||||||
if (!tags) return null
|
if (!tags) return null
|
||||||
@ -51,13 +124,15 @@ const Tags = () => {
|
|||||||
const popularTags = _.take(_.sortBy(tags, [t => -1 * t.numArticles]), 10)
|
const popularTags = _.take(_.sortBy(tags, [t => -1 * t.numArticles]), 10)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Paper className={classes.root}>
|
||||||
Popular tags: { popularTags.map(tag => (
|
<Typography variant="subtitle1" className={classes.title}>Popular tags</Typography>
|
||||||
<div>
|
{ popularTags.map(tag => (
|
||||||
{ tag.name } ({ tag.numArticles })
|
<Chip
|
||||||
</div>
|
className={classes.chip}
|
||||||
))}
|
label={`${tag.name} (${tag.numArticles})`}
|
||||||
</div>
|
/>
|
||||||
|
))}
|
||||||
|
</Paper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,26 +1,57 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
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'
|
import useAuth from '@wasp/auth/useAuth.js'
|
||||||
|
|
||||||
|
|
||||||
const Navbar = () => {
|
const useStyles = makeStyles((theme) => ({
|
||||||
const { data: user } = useAuth()
|
root: {
|
||||||
|
flexGrow: 1,
|
||||||
|
marginBottom: 50
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
|
const { data: user } = useAuth()
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classes.root}>
|
||||||
<Link to='/'> Home </Link>
|
<AppBar position="static">
|
||||||
<Link to='/editor'> New Article </Link>
|
<Toolbar>
|
||||||
<Link to='/settings'> Settings </Link>
|
<Typography variant="h6" className={classes.title}>Conduit</Typography>
|
||||||
<Link to={`/@${user.username}`}> { user.username } </Link>
|
|
||||||
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classes.root}>
|
||||||
<Link to='/login'> Sign in </Link>
|
<AppBar position="static">
|
||||||
<Link to='/register'> Sign up </Link>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,13 @@ import React, { useState } from 'react'
|
|||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
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 logout from '@wasp/auth/logout.js'
|
||||||
import { useQuery } from '@wasp/queries'
|
import { useQuery } from '@wasp/queries'
|
||||||
|
|
||||||
@ -11,6 +18,27 @@ import getArticle from '@wasp/queries/getArticle'
|
|||||||
|
|
||||||
import Navbar from '../../Navbar'
|
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) => {
|
const ArticleEditorPage = (props) => {
|
||||||
// TODO: Here, as in some other places, it feels tricky to figure out what is happening regarding the state.
|
// 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
|
// 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
|
return articleError
|
||||||
? articleError.message || articleError
|
? articleError.message || articleError
|
||||||
: (
|
: (
|
||||||
<div>
|
<Container maxWidth="lg">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<ArticleEditor user={props.user} article={article} />
|
<Grid container direction="row" justify="center">
|
||||||
</div>
|
<Grid item xs={8}>
|
||||||
|
<ArticleEditor user={props.user} article={article} />
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,8 +64,9 @@ ArticleEditorPage.propTypes = {
|
|||||||
user: PropTypes.object
|
user: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ArticleEditor = (props) => {
|
const ArticleEditor = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const user = props.user
|
const user = props.user
|
||||||
const article = props.article
|
const article = props.article
|
||||||
|
|
||||||
@ -85,30 +118,37 @@ const ArticleEditor = (props) => {
|
|||||||
) }
|
) }
|
||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<h2>Article title</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type='text'
|
label="Article Title"
|
||||||
value={title}
|
fullWidth
|
||||||
|
value={title}
|
||||||
onChange={e => setTitle(e.target.value)}
|
onChange={e => setTitle(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>What's this article about?</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type='text'
|
label="What's this article about"
|
||||||
value={description}
|
fullWidth
|
||||||
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={e => setDescription(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Markdown content</h2>
|
<TextField
|
||||||
<textarea
|
className={classes.textField}
|
||||||
value={markdownContent}
|
label="Markdown content"
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
fullWidth
|
||||||
|
value={markdownContent}
|
||||||
onChange={e => setMarkdownContent(e.target.value)}
|
onChange={e => setMarkdownContent(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Enter tags</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type="text"
|
label="Enter tags"
|
||||||
value={newTagName}
|
fullWidth
|
||||||
|
value={newTagName}
|
||||||
onChange={e => setNewTagName(e.target.value)}
|
onChange={e => setNewTagName(e.target.value)}
|
||||||
onKeyPress={e => {
|
onKeyPress={e => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
@ -118,18 +158,14 @@ const ArticleEditor = (props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className={classes.tags}>
|
||||||
{ tags.map(tag => (
|
{ tags.map(tag => (
|
||||||
<div key={tag.name}>
|
<Chip label={tag.name} onDelete={() => setTags(tags.filter(t => t !== tag))}/>
|
||||||
{tag.name}
|
|
||||||
<button onClick={() => setTags(tags.filter(t => t !== tag))}> X </button>
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<Button type='submit' color='primary' variant='contained'>Publish Article</Button>
|
||||||
<input type='submit' value='Publish Article' />
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -2,12 +2,40 @@ import React from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import moment from 'moment'
|
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 setArticleFavorited from '@wasp/actions/setArticleFavorited'
|
||||||
|
|
||||||
import smileyImageUrl from '../../smiley.jpg'
|
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 ArticleList = (props) => {
|
||||||
const articles = props.articles
|
const articles = props.articles
|
||||||
|
|
||||||
return articles ? (
|
return articles ? (
|
||||||
<div>
|
<div>
|
||||||
{ articles.map(article => <Article article={article} key={article.id} />) }
|
{ articles.map(article => <Article article={article} key={article.id} />) }
|
||||||
@ -16,6 +44,7 @@ const ArticleList = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Article = (props) => {
|
const Article = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
const article = props.article
|
const article = props.article
|
||||||
|
|
||||||
const toggleArticleFavorited = async () => {
|
const toggleArticleFavorited = async () => {
|
||||||
@ -23,26 +52,42 @@ const Article = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ border: '1px solid black' }}>
|
<Card className={classes.article} elevation={2}>
|
||||||
<Link to={`/article/${article.slug}`}>
|
<CardHeader
|
||||||
<h2> { article.title } </h2>
|
avatar={<Avatar>A</Avatar>}
|
||||||
</Link>
|
title={article.user.username}
|
||||||
<p> { article.description } </p>
|
subheader={moment(article.createdAt).format('MMMM DD, YYYY')}
|
||||||
<p>
|
/>
|
||||||
<em> Tags: </em>
|
|
||||||
{ article.tags.map(t => t.name).join('; ') }
|
<CardContent>
|
||||||
</p>
|
<Typography variant="h5">
|
||||||
<p>
|
<Link to={`/article/${article.slug}`}>
|
||||||
<img src={ article.user.profilePictureUrl || smileyImageUrl } width='30px' />
|
{ article.title }
|
||||||
<div> { article.user.username } </div>
|
</Link>
|
||||||
<div> { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
|
</Typography>
|
||||||
</p>
|
<Typography variant="body2" color="textSecondary" component="p">
|
||||||
<div>
|
{ article.description }
|
||||||
<button onClick={toggleArticleFavorited}>
|
</Typography>
|
||||||
|
|
||||||
|
|
||||||
|
</CardContent>
|
||||||
|
<CardActions disableSpacing>
|
||||||
|
<Button onClick={toggleArticleFavorited} size="small" color="primary">
|
||||||
{ article.favorited ? 'Unlike' : 'Like' } ({ article.favoritesCount })
|
{ article.favorited ? 'Unlike' : 'Like' } ({ article.favoritesCount })
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
|
||||||
</div>
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,19 @@ import ReactMarkdown from 'react-markdown'
|
|||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import PropTypes from 'prop-types'
|
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 useAuth from '@wasp/auth/useAuth.js'
|
||||||
import { useQuery } from '@wasp/queries'
|
import { useQuery } from '@wasp/queries'
|
||||||
|
|
||||||
@ -16,7 +29,35 @@ import deleteComment from '@wasp/actions/deleteComment'
|
|||||||
|
|
||||||
import Navbar from '../../Navbar'
|
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 ArticleViewPage = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { data: me } = useAuth({ keepPreviousData: true })
|
const { data: me } = useAuth({ keepPreviousData: true })
|
||||||
|
|
||||||
@ -50,59 +91,77 @@ const ArticleViewPage = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return article ? (
|
return article ? (
|
||||||
<div>
|
<Container maxWidth="lg">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<div>
|
<Grid container direction="row" justify="center">
|
||||||
<div> Author: { article.user.username } </div>
|
<Grid item xs={8}>
|
||||||
<div> Created at: { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
|
<Typography variant="h2">{ article.title }</Typography>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p> { article.title } </p>
|
<div> Author: { article.user.username } </div>
|
||||||
<p> { article.description } </p>
|
<div> Created at: { moment(article.createdAt).format('MMMM DD, YYYY') } </div>
|
||||||
<p>
|
</div>
|
||||||
<ReactMarkdown children={article.markdownContent} />
|
</Grid>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Tags: { article.tags.map(tag => <div> {tag.name} </div>) }
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ isMyArticle && (
|
<Grid item xs={8}>
|
||||||
<div>
|
<p>
|
||||||
<button onClick={handleEditArticle}> Edit Article </button>
|
<Typography variant="h5">
|
||||||
<button onClick={handleDeleteArticle}> Delete Article </button>
|
<ReactMarkdown children={article.markdownContent} />
|
||||||
</div>
|
</Typography>
|
||||||
)}
|
</p>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Comments article={article}/>
|
<Grid item xs={8}>
|
||||||
</div>
|
<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
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
const Comments = (props) => {
|
const Comments = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const article = props.article
|
const article = props.article
|
||||||
|
|
||||||
const { data: me } = useAuth()
|
const { data: me } = useAuth()
|
||||||
|
|
||||||
const { data: comments } = useQuery(getArticleComments, { articleId: article.id })
|
const { data: comments } = useQuery(getArticleComments, { articleId: article.id })
|
||||||
|
|
||||||
return comments ? (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ me
|
{ me
|
||||||
? <CreateComment article={article} />
|
? <CreateComment article={article} />
|
||||||
: null // TODO: Instead of nothing, tell them they need to sign up / sign in to comment.
|
: null // TODO: Instead of nothing, tell them they need to sign up / sign in to comment.
|
||||||
}
|
}
|
||||||
|
{ comments ? (
|
||||||
<div>
|
<div className={classes.comments}>
|
||||||
{ comments.length
|
{ comments.length
|
||||||
? comments.map(c => <Comment comment={c} key={c.id} />)
|
? comments.map(c => <Comment comment={c} key={c.id} />)
|
||||||
: 'No comments yet!'
|
: 'No comments yet!'
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
) : null }
|
||||||
</div>
|
</div>
|
||||||
) : null
|
)
|
||||||
}
|
}
|
||||||
Comments.propTypes = {
|
Comments.propTypes = {
|
||||||
article: PropTypes.object.isRequired
|
article: PropTypes.object.isRequired
|
||||||
@ -123,17 +182,28 @@ const Comment = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ border: '1px solid black', width: '300px' }}>
|
<>
|
||||||
<div> { comment.content } </div>
|
<Card>
|
||||||
<div> { moment(comment.createdAt).format('MMMM DD, YYYY') } </div>
|
<CardHeader
|
||||||
{ /* TODO: Show user's profile picture. */ }
|
avatar={<Avatar>R</Avatar>}
|
||||||
{ /* TODO: Make username a link to the user profile. */ }
|
title={comment.user.username}
|
||||||
<div> { comment.user.username } </div>
|
subheader={ moment(comment.createdAt).format('MMMM DD, YYYY') }
|
||||||
{ (me && me.id === comment.userId)
|
/>
|
||||||
? <button onClick={onDelete}> Delete </button>
|
|
||||||
: null
|
<CardContent>
|
||||||
}
|
<Typography variant="body1">
|
||||||
</div>
|
{ comment.content }
|
||||||
|
</Typography>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
<CardActions>
|
||||||
|
{ (me && me.id === comment.userId)
|
||||||
|
? <Button size="small" color="primary" onClick={onDelete}>Delete</Button>
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
</CardActions>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Comment.propTypes = {
|
Comment.propTypes = {
|
||||||
@ -141,6 +211,8 @@ Comment.propTypes = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateComment = (props) => {
|
const CreateComment = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const article = props.article
|
const article = props.article
|
||||||
|
|
||||||
const [content, setContent] = useState('')
|
const [content, setContent] = useState('')
|
||||||
@ -157,13 +229,17 @@ const CreateComment = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<textarea
|
<TextField
|
||||||
|
className={classes.textField}
|
||||||
|
label='Leave a comment'
|
||||||
|
multiline
|
||||||
|
fullWidth
|
||||||
|
rows={3}
|
||||||
value={content}
|
value={content}
|
||||||
onChange={e => setContent(e.target.value)}
|
onChange={e => setContent(e.target.value)}
|
||||||
style={{ width: '300px' }}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
<div className={classes.postCommentButton}>
|
||||||
<input type='submit' value='Post Comment' />
|
<Button type="submit" color="primary" variant="contained">Post Comment</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
@ -88,10 +88,10 @@ export const getArticle = async ({ slug }, context) => {
|
|||||||
return article
|
return article
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getArticleComments = async ({ slug }, context) => {
|
export const getArticleComments = async ({ articleId }, context) => {
|
||||||
// TODO: Do some error handling?
|
// TODO: Do some error handling?
|
||||||
const comments = await context.entities.Comment.findMany({
|
const comments = await context.entities.Comment.findMany({
|
||||||
where: { article: { slug } },
|
where: { articleId },
|
||||||
include: {
|
include: {
|
||||||
user: {
|
user: {
|
||||||
// TODO: Tricky, if you forget this you could return unwanted fields
|
// TODO: Tricky, if you forget this you could return unwanted fields
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
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 useAuth from '@wasp/auth/useAuth.js'
|
||||||
import { useQuery } from '@wasp/queries'
|
import { useQuery } from '@wasp/queries'
|
||||||
|
|
||||||
@ -12,7 +21,16 @@ import Navbar from '../../Navbar'
|
|||||||
import ArticleListPaginated from '../../article/components/ArticleListPaginated'
|
import ArticleListPaginated from '../../article/components/ArticleListPaginated'
|
||||||
import smileyImageUrl from '../../smiley.jpg'
|
import smileyImageUrl from '../../smiley.jpg'
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
articles: {
|
||||||
|
marginTop: theme.spacing(5)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
const UserProfilePage = (props) => {
|
const UserProfilePage = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
const { data: me } = useAuth()
|
const { data: me } = useAuth()
|
||||||
@ -30,25 +48,32 @@ const UserProfilePage = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return user ? (
|
return user ? (
|
||||||
<div>
|
<Container maxWidth="lg">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<img src={user.profilePictureUrl || smileyImageUrl} />
|
<Grid container direction="row" justify="center">
|
||||||
<p> { user.username } </p>
|
<Grid item xs={8}>
|
||||||
<p> { user.bio } </p>
|
<img src={user.profilePictureUrl || smileyImageUrl} />
|
||||||
{ me && me.username === username && (
|
<p> { user.username } </p>
|
||||||
<div>
|
<p> { user.bio } </p>
|
||||||
<Link to='/settings'>Edit Profile Settings</Link>
|
{ me && me.username === username && (
|
||||||
</div>
|
<div>
|
||||||
)}
|
<Button component={ Link } to="/settings" variant="contained" color="primary">
|
||||||
{ me && me.username !== username && (
|
Edit Profile Settings
|
||||||
<div>
|
</Button>
|
||||||
<FollowUserButton user={user} />
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
{ me && me.username !== username && (
|
||||||
|
<div>
|
||||||
|
<FollowUserButton user={user} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Articles user={user} />
|
<Articles user={user} />
|
||||||
</div>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Container>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,23 +97,72 @@ const FollowUserButton = (props) => {
|
|||||||
) : null
|
) : 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 Articles = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const user = props.user
|
const user = props.user
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={classes.articles}>
|
||||||
<h1> My Articles </h1>
|
<ProfileFeedTabs {...props} />
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { Link, useHistory } from 'react-router-dom'
|
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 logout from '@wasp/auth/logout.js'
|
||||||
|
|
||||||
import updateUser from '@wasp/actions/updateUser'
|
import updateUser from '@wasp/actions/updateUser'
|
||||||
@ -9,16 +15,31 @@ import Navbar from '../../Navbar'
|
|||||||
|
|
||||||
const UserSettingsPage = ({ user }) => {
|
const UserSettingsPage = ({ user }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Container maxWidth="lg">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
<Grid container direction="row" justify="center">
|
||||||
<UserSettings user={user}/>
|
<Grid item xs={6}>
|
||||||
</div>
|
<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 UserSettings = (props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
|
||||||
const user = props.user
|
const user = props.user
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
@ -64,46 +85,59 @@ const UserSettings = (props) => {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
|
|
||||||
<h2>URL of profile picture</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type='text'
|
label="URL of profile picture"
|
||||||
value={profilePictureUrl}
|
fullWidth
|
||||||
|
value={profilePictureUrl}
|
||||||
onChange={e => setProfilePictureUrl(e.target.value)}
|
onChange={e => setProfilePictureUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Username</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type='text'
|
label="Username"
|
||||||
value={username}
|
fullWidth
|
||||||
|
value={username}
|
||||||
onChange={e => setUsername(e.target.value)}
|
onChange={e => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Short bio</h2>
|
<TextField
|
||||||
<textarea
|
className={classes.textField}
|
||||||
value={bio}
|
label="Short bio"
|
||||||
|
multiline
|
||||||
|
rows={3}
|
||||||
|
fullWidth
|
||||||
|
value={bio}
|
||||||
onChange={e => setBio(e.target.value)}
|
onChange={e => setBio(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>Email</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type='text'
|
label="Email"
|
||||||
value={email}
|
fullWidth
|
||||||
|
value={email}
|
||||||
onChange={e => setEmail(e.target.value)}
|
onChange={e => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h2>New password</h2>
|
<TextField
|
||||||
<input
|
className={classes.textField}
|
||||||
type='password'
|
label="New password"
|
||||||
value={newPassword}
|
type="password"
|
||||||
|
fullWidth
|
||||||
|
value={newPassword}
|
||||||
onChange={e => setNewPassword(e.target.value)}
|
onChange={e => setNewPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<Button type="submit" color="primary" variant="contained">Update Settings</Button>
|
||||||
<input type='submit' value='Update Settings' />
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<button onClick={handleLogout}> Log out </button>
|
<Button
|
||||||
|
className={classes.logoutButton}
|
||||||
|
type="submit" color="secondary"
|
||||||
|
variant="contained" onClick={handleLogout}
|
||||||
|
>
|
||||||
|
Log out
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
app Conduit {
|
app Conduit {
|
||||||
title: "Conduit"
|
title: "Conduit",
|
||||||
|
|
||||||
|
head: [
|
||||||
|
"<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap\" />"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
auth {
|
auth {
|
||||||
@ -187,5 +191,7 @@ dependencies {=json
|
|||||||
"prop-types": "15.7.2",
|
"prop-types": "15.7.2",
|
||||||
"react-markdown": "5.0.3",
|
"react-markdown": "5.0.3",
|
||||||
"moment": "2.29.1",
|
"moment": "2.29.1",
|
||||||
|
"@material-ui/core": "4.11.3",
|
||||||
|
"@material-ui/icons": "4.11.2",
|
||||||
"slug": "4.0.2"
|
"slug": "4.0.2"
|
||||||
json=}
|
json=}
|
||||||
|
Loading…
Reference in New Issue
Block a user