Creating a forum with React and Appwrite – Part 3
Tags: javascript, opensource, appwrite, webdev
Published: 2022-04-23 00:18:58

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](https://dev.to/swinkid/creating-a-forum-with-react-and-appwrite-part-2-498b). 

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!

![Image description](https://media.giphy.com/media/fyEJAvz1bJpde/giphy.gif)

## 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:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p36s794mefsmz5ll5e9c.jpg)

### 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:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1lt3dal3d46dkjvaai6m.jpg)

Categories collection:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jh3hip0pomffswocyj6q.jpg)

## 🛠️ 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:

```console
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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:

```javascript
<Route path="/post" element={<Post />}/>
```

Finally, let's make posts clickable! Open `src/Components/Forum/Posts/PostItem/PostItem.js` and update `<CardActionArea>` to:

```javascript
<CardActionArea onClick={() => {
    navigate(`/post?id=${id}`);
}}>
```

You may also need to add this in the same function (under `export function PostItem(props) {`):

```javascript
const navigate = useNavigate();
```
You should now be able to add new posts, for example:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/93ljz8t9o4usy9n3n5ic.gif)

Also, if you login as another user you can see other comments and posts:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7twn76mk9gkgoywb56g1.gif)

## 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!

## 📚 Learn more
- [Appwrite's Docs](https://appwrite.io/docs)
- [Appwrite's Discord](https://discord.gg/GSeTUeA)
- [My Github](https://github.com/Swinkid)
- [Twitter](https://twitter.com/Swinkid)
- [Project Github](https://github.com/Swinkid/appwrite-forum)