Creating a forum with React and Appwrite – Part 3

Welcome back to our ongoing series on crafting a fully functional forum using React and Appwrite! If you haven’t checked out Part 2 yet, make sure to explore it here.

This chapter focuses on enabling users to post new discussions and engage in comments. Prepare for an in-depth guide, so have your tea and snacks ready!

Image description

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

Attributes

Navigate to your Appwrite Console and select ‘Database’. We require a new collection specifically for storing comments related to articles. Create a new collection by following the instructions provided:

For managing attributes, visit the newly created collection’s attributes section and add the following:

Attribute ID Type Size Required Array Default Value
postId String 255 Yes    
userId String 255 Yes    
content String 255 No    
author String 255 No    

Additionally, set up the necessary indexes for optimizing queries:

Index Key Type Attributes
userId key userId (ASC)
postId key categoryId (ASC)

Don’t forget to configure the collection permissions appropriately to enhance security and manageability, adjusting them as necessary for admin-level modifications in future steps.

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

Categories collection: Image description

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

Image description

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

Image description

Conclusion

By following these steps, you’ll establish a basic yet operational forum platform, capable of listing categories, topics, and displaying comments.

Whats next?

Moving forward, our articles will focus on integrating smaller, incremental features to enhance our forum’s functionality.

Stay tuned for our upcoming sub-series, which will transition our project to utilize AWS’ Amplify, incorporating Lambda functions, API Gateway, and Cognito for a more robust infrastructure.

I’m eager to hear your thoughts and suggestions for future features! Feel free to reach out via Twitter or leave a comment below with your ideas.

📚 Learn more