Creating a forum with React and Appwrite – Part 3

alexcodes

Alex

Posted on April 23, 2022

Creating a forum with React and Appwrite – Part 3

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!

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

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

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
Enter fullscreen mode Exit fullscreen mode

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);
},
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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} />
        ))}
    </>
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
            ))}
        </>
    );
}
Enter fullscreen mode Exit fullscreen mode

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 />}/>
Enter fullscreen mode Exit fullscreen mode

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

<CardActionArea onClick={() => {
    navigate(`/post?id=${id}`);
}}>
Enter fullscreen mode Exit fullscreen mode

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

const navigate = useNavigate();
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
alexcodes
Alex

Posted on April 23, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related