mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-24 17:44:21 +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 _ 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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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=}
|
||||
|
Loading…
Reference in New Issue
Block a user