Welcome to the second part in this multi-part series on creating a Forum with React and Appwrite. If you havn't seen it already, go and checkout part 2 here.
In this installment we're aiming to be able to add new posts and comment on them. It's going to be a meaty one, so grab your cup of tea and snacks!
Database
As with any new part of this series, we need to get a few things ironed out in the database.
Firstly head over to your Appwrite Console and click 'Database'. We're going to need a new collection to hold our comments for the articles. Click add collection and fill out the prompt like below:
Attributes
Head over to the attributes tab for the collection you just created and add the following attributes:
Attribute ID | Type | Size | Required | Array | Default Value |
---|---|---|---|---|---|
postId | String | 255 | Yes | ||
userId | String | 255 | Yes | ||
content | String | 255 | No | ||
author | String | 255 | No |
Indexes
Head over to the Indexes tab for the collection you just created and add the following Indexes:
Index Key | Type | Attributes |
---|---|---|
userId | key | userId (ASC) |
postId | key | categoryId (ASC) |
Collection Permissions
One thing I've forgotten to mention throughout the series is you'll need to setup your collection permissions. By default it's set to collection wide. We dont want this.
Later on in the series we may need to adjust some permissions to allow things to be edited by an Administrator. But for now, go through each of your collection settings and double check they're set to the following:
Profiles, Posts and Comments collections:
🛠️ On The Tools
With the pleasantries out the way, let's get cracking! Head over to your .env file and add the following to the bottom of the file:
REACT_APP_COMMENTS_COLLECTION=6263216f884ae458a235
Make sure you replace 6263216f884ae458a235
with the comments collection id found in your appwrite console.
Create Documents
We need to add some code into src/Services/api.js
to provide an interface for our UI to be able to create new doucmnets into our database. Add the following somewhere into the file:
createDocument: (collectionId, data, read, write) => {
return api.provider().database.createDocument(collectionId, 'unique()', data, read, write);
},
Essentially what we're doing here is telling AppWrite's SDK to call the REST endpoint that handles document creation with a unique ID along with the permission and data information for the document.
New Post
Open src/Components/Forum/Posts/NewPostButton/NewPostButton.js
and update it to look like the following:
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 400,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
};
export function NewPostButton(props) {
const {REACT_APP_POSTS_COLLECTION} = process.env;
const user = useSelector((state) => state.user);
const [isLoggedIn, setIsLoggedIn] = useState(user.isLoggedIn);
const [open, setOpen] = React.useState(false);
const [title, setTitle] = React.useState('');
const [content, setContent] = React.useState('');
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
useEffect(() => {
setIsLoggedIn(user.isLoggedIn);
});
function submitPost(){
let {fetchPosts, id} = props;
api.createDocument(REACT_APP_POSTS_COLLECTION, {
'categoryId': id,
'userId': user.account.$id,
'title': title,
'content': content,
'author': user.account.name,
}, ['role:all']).then(() => {
setTitle('');
setContent('');
handleClose();
fetchPosts();
})
}
return isLoggedIn ? (
<>
<Button style={{marginTop: '1rem'}} variant="contained" color="primary" onClick={handleOpen} disableElevation>New Post</Button>
<Modal
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
<Typography id="modal-modal-title" variant="h6" component="h2">
New Post
</Typography>
<TextField
fullWidth
label="Tile"
id="title"
sx={{mt: 1}}
value={title}
onChange={(e) => {setTitle(e.target.value)}}
/>
<TextField
sx={{mt: 1}}
id="content"
label="Content"
fullWidth
multiline
rows={4}
onChange={(e) => {setContent(e.target.value)}}
/>
<Button sx={{mt: 1}} variant="contained" onClick={() => submitPost()}>Submit</Button>
</Box>
</Modal>
</>
) : null;
}
Your also going to need to update src/Components/Forum/Posts/Posts.js
to pass through the category id through the props to the child component:
return (
<>
<Grid container>
<Grid item xs={6}>
<NewPostButton id={searchParams.get("id")} fetchPosts={fetchPosts}/>
</Grid>
<Grid item xs={6} style={{textAlign: 'right'}}>
<BackButton/>
</Grid>
</Grid>
{posts.map((post) => (
<PostItem title={post.title} description={post.description} author={post.author} key={post.$id} id={post.$id} />
))}
</>
);
Add Comment
We're going to need a new button to click to create a new comment.
It's very similar to the new post button. We could refactor it to leverage it for both scenarios; but I'm lazy. We will revisit this but for now, create a new file src/Components/Post/Components/NewCommentButton/NewCommentButton.js
with the following:
export function NewCommentButton(props) {
const user = useSelector((state) => state.user);
const [isLoggedIn, setIsLoggedIn] = useState(user.isLoggedIn);
useEffect(() => {
setIsLoggedIn(user.isLoggedIn);
});
return isLoggedIn ? <Button style={{marginTop: '1rem'}} variant="contained" color="primary" disableElevation>New
Comment</Button> : null;
}
View Post & Comments
Lets render the post and comments! Create a new file src/Components/Post/Post.js
with the following content:
export function Post(props) {
const {REACT_APP_COMMENTS_COLLECTION, REACT_APP_POSTS_COLLECTION} = process.env;
let [comments, setComments] = useState([]);
let [post, setPost] = useState({});
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
function fetchComments() {
api.listDocuments(REACT_APP_COMMENTS_COLLECTION, [Query.equal('postId', searchParams.get("id"))]).then((result) => {
setComments(result.documents);
});
}
function fetchPost(){
api.getDocument(REACT_APP_POSTS_COLLECTION, searchParams.get("id")).then((post) => {
setPost(post)
});
}
useEffect(() => {
if (searchParams.get("id")) {
fetchComments();
fetchPost();
} else {
navigate('/');
}
}, []);
return (
<>
<Grid container>
<Grid item xs={6}>
<NewCommentButton id={searchParams.get("id")} fetchComments={fetchComments}/>
</Grid>
<Grid item xs={6} style={{textAlign: 'right'}}>
<BackButton/>
</Grid>
</Grid>
<Card style={{marginTop: '1rem'}}>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
{post?.title}
</Typography>
<Typography variant="body2" color="text.secondary">
{post?.content}
</Typography>
<Typography variant="body2" color="text.secondary">
by {post?.author}
</Typography>
</CardContent>
</Card>
{comments.map((comment) => (
<Card style={{marginTop: '1rem'}}>
<CardContent>
<Typography variant="body2" color="text.secondary">
{comment?.content}
</Typography>
<Typography variant="body2" color="text.secondary">
by {comment?.author}
</Typography>
</CardContent>
</Card>
))}
</>
);
}
Final Adjustments
Now we've got the leg work out the way, let's make some adjustments so what you've developed is useable. Head over to your App.js
file to add a new route. Under your 'posts' route, add the following:
<Route path="/post" element={<Post />}/>
Finally, let's make posts clickable! Open src/Components/Forum/Posts/PostItem/PostItem.js
and update <CardActionArea>
to:
<CardActionArea onClick={() => {
navigate(`/post?id=${id}`);
}}>
You may also need to add this in the same function (under export function PostItem(props) {
):
const navigate = useNavigate();
You should now be able to add new posts, for example:
Also, if you login as another user you can see other comments and posts:
Conclusion
By now you should have a fairly basic, but working message board. You can now list categories and topics aswell as view comments. From now on the articles will be much more 'byte sized'; Focussing on adding smaller features rather than larger pieces of work.
As always, hit me up on twitter or comment here if I've missed something or you need something clarifying.
Whats next?
We're going to carry on adding features in future articles. I'm also doing a 'Sub series' that takes the finished project and converts it into AWS' Amplify instead of Appwrite with Lambda functions, API Gateway and Icognito! Chuck us a follow on Twitter or on Dev.to if you want to be the first to know.
What features do you want to see added? Drop a comment or get in touch with suggestions!
Top comments (0)