DEV Community

Cover image for Building A Blog App With React JS and Fauna
Babatunde Koiki
Babatunde Koiki

Posted on • Updated on

Building A Blog App With React JS and Fauna

Authored in connection with the Write with Fauna program.

Introduction

There are several ways to build a web app in 2021; a good practice is to use Single Page Applications(SPA). If you’re considering building a SPA, React is one framework that’s good to use. There are a couple of reasons why you should choose React Js for your single-page application. Some of which are speed, simplicity, reusability.

When building a serverless full-stack app, you might be considering a serverless database to use. Fauna is a database that helps save time by using existing infrastructure to build web applications without setting up a custom API server.

Alt Text

This article will be walking you through how I built a Blog App with React, Fauna, Cloudinary, Bootstrap, and CKEditor.

Prerequisites

To take full advantage of this article, you need to have the following installed on your laptop.

  1. Node JS
  2. Have access to one package manager such as npm or yarn
  3. Create-react-app, a CLI tool installed as a global package or use npx
  4. Access to FaunaDB dashboard
  5. Basic knowledge of React Hooks

Getting Started With FaunaDB

First, create an account with Fauna

Alt Text

Creating A Fauna Database

To create a fauna database, first head to the fauna dashboard.

Alt Text

Next, click on the New Database button and enter the database name, then click enter.

Creating Fauna Collections

A collection is simply a grouping of documents(rows) with the same or a similar purpose. A collection acts similarly to a table in a traditional SQL database.

In the app we’re creating, we’ll have two collections, users and blogs. The user collection is where we’ll be storing our user data, while the blog collection is where we’ll be keeping all the blog data. To create these collections, click on the database you created, click New Collection Enter only the collection name (users), then click save and do the same for the second collection (blogs).

Alt Text

Creating Fauna Indexes

Indexes are used to quickly find data without searching every document in a database collection every time a database collection is accessed. Indexes can be created using one or more fields of a database collection. To create a fauna index, click on the indexes section in the left part of your dashboard.

Alt Text

In our app, we need the following indexes:

  1. all_blogs: This index is what we’ll use to retrieve all the created blogs. This index doesn’t have any terms and values.
  2. blogs_by_author: This index is what we’ll use to retrieve all blogs created by a particular user. The terms field will be data.author.username.
  3. user_by_email: This index is what we’ll use to get a user’s data with a given email. This index needs to be unique so the collection doesn’t have duplicate emails.
  4. user_by_username: This index is what we’ll use to get a user’s data with a given username. This index needs to be unique, so the collection doesn’t have a duplicate username. We won’t be using this index in our application, but it helps us validate that no same username is created in the collection.

Generating Your Fauna Secret Key

Fauna secret key is used to connect to fauna in an application or script, and it is unique per database. To generate your key, go to your dashboard’s security section and click on New Key. Enter your key name. A new key will be generated for you. Keep the key somewhere safe as you can’t have access to that key in the dashboard again.

Alt Text

Setting Up The Application

On the command line, type the following command wherever you want to store your project.

Create-react-app react-blog 
npm i @ckeditor/ckeditor5-react
npm i @fortawesome/react fontawesome axios bcryptjs 
npm i bootstrap dotenv faunadb react-router-dom
Enter fullscreen mode Exit fullscreen mode

The command above will create a folder named react-blog and some boilerplate files. Delete all the files in your src folder except index.js and App.js.

Create the following files in your src folder

  1. App.js: This is the file that combines all the components and arranges them in the order we want. It displays the components in the src folder the way we want them to be displayed.
  2. index.js: This file uses React to render the components in the App.js.
  3. models.js: This is the file we use to communicate to the fauna database.
  4. components/BlogPreview.js: This file is where we create our blog preview component that will be displayed for a single blog on the home page.
  5. components/Navbar.js: This is where we make the navbar component for our application.
  6. components/Signout.js: This is where we make the signout component for our application.
  7. screens/Blog.js: This is the page where we’ll be rendering a single blog view.
  8. screens/CreateBlog.js: This is the page where we’ll be creating a new blog.
  9. screens/HomePage.js: This is the page that shows all the blogs. This component is the home page of our app. It uses the blog preview component
  10. screens/NotFound.js: This page is the 404 page of our app.
  11. screens/Signin.js: This is the sign-in page of our app.
  12. screens/Signup.js: This is the signup page of our app.

Let’s start by creating our models. Before we can write any code; we need to paste the secret key we got from fauna in an environment file:

Create a .env file in the root directory of your project and type the following:

REACT_APP_FAUNA_KEY='secret key generated from fauna.'
Enter fullscreen mode Exit fullscreen mode

In your index.js file, add the below the imports of the file:

import 'bootstrap/dist/css/bootstrap.min.css';
Enter fullscreen mode Exit fullscreen mode

Database Setup

In your models.js file type the following:

import faunadb, {query as q} from 'faunadb'
import bcrypt from 'bcryptjs'
import dotenv from 'dotenv'

dotenv.config()
const client = new faunadb.Client({secret: process.env.REACT_APP_FAUNA_KEY})

export  const createUser = async (name, email, username, password) => {
  password = bcrypt.hashSync(password, bcrypt.genSaltSync(10)) //hashes the password 
  let data
  try {
    data= await client.query(
      q.Create(
        q.Collection('users'),
        {
          data: {
            name, 
            email, 
            username, 
            password
          }
        }
      )
    )
    if (data.name === 'BadRequest') return // if there's an error in the data creation
  } catch (error) {
    return 
  }
  const user = data.data
  user.id = data.ref.value.id // attaches the ref id as the user id in the client
  return user
}

export const getUser = async (userId) => {
  try {
    const user = await client.query(
      q.Get(
        q.Ref(q.Collection('users'), userId)
      )
    )
    return user.data
  } catch {
    return // return null if there is any error.
  }
}

export const loginUser = async (email, password) => {
 try {
  let userData = await client.query(
    q.Get(
      q.Match(q.Index('user_by_email'), email)
    )
  )
  userData.data.id = userData.ref.value.id
  if (bcrypt.compareSync(password, userData.data.password)) return userData.data
  else return
 } catch (error) {
   return
 }
}

export const createPost = async (title, body, avatar, authorId, tags) => {
  const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"] 
  let author = await getUser(authorId)
  const date = new Date()
  let data = await client.query(
    q.Create(
      q.Collection('blogs'),
      {
        data: {
          title, 
          body, 
          upvote: 0,
          downvote: 0,
          created__at: `${months[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`, // converts date to "Month day, Year"
          author: {
            name:author.name, 
            email: author.email, 
            id:author.id, 
            username: author.username
          },
          avatar,
          tags
        }
      }
    )
  )
  data.data.id = data.ref.value.id
  return data.data
}

export const getPosts = async () => {
  let allBlogs = await client.query(
    q.Map(
      q.Paginate(q.Documents(q.Collection("blogs"))),
      q.Lambda("X", q.Get(q.Var("X")))
    )
  )
  return allBlogs.data
}

export const getPost = async id => {
  try {
    let blog = await client.query(
      q.Get(q.Ref(q.Collection('blogs'), id))
    )
    blog.data.id = blog.ref.value.id
    return blog.data
  } catch (error) {
    return
  }
}

export const upvotePost = async (upvote, id) => {
  try {
    let blog = await client.query(
      q.Update(
        q.Ref(q.Collection('blogs'), id),
        {data: {upvote}}
      )
    )
    blog.data.id = blog.ref.value.id
    return blog.data
  } catch  {
    return
  }
}

export const downvotePost = async (downvote, id) => {
  try {
    let blog = await client.query(
      q.Update(
        q.Ref(q.Collection('blogs'), id),
        {data: {downvote}}
      )
    )
    blog.data.id = blog.ref.value.id
    return blog.data
  } catch (error) {
    return
  }
}
Enter fullscreen mode Exit fullscreen mode

In the models.js file above, I created a fauna client using the secret key obtained from the environment variable. Then I created multiple helper functions. Let’s go over each of them.

  1. createUser: This is the function used to create a new user, we need just the name, email, username, and password of the user, and we return the created data
  2. getUser: This is the function used to get user data given its fauna id, which is in the Ref object when we run client.query. While returning data, I added this id, which I used as the app’s id on the client-side for simplicity.
  3. loginUser: This is the function used to verify a user object using email and password. If there is data with the given email and the password is correct, I returned the user data and null if otherwise.
  4. createPost: This is the function used to create a new blog post. I used the getUser function to get the user data of the user creating the blog post given its userId.
  5. getPosts: This is the function used to retrieve all blog posts.
  6. getPost: This is the function used to get a single blog post given its unique id.
  7. upvotePost and downvotePost: These functions are used to upvote and downvote a post, respectively.

Navbar component

In your Navbar.js file, type the following:

import React from "react";
import { Link, useHistory, useLocation } from "react-router-dom";

const DynamicSignup = ({isLoggedIn}) => {
  const {pathname} = useLocation() // endpoint of the request
  const history = useHistory() 

  const handleSignout = () => {
    localStorage.clear()
    history.push('/') //redirects back to homepage
  }
  if (isLoggedIn) {
    return (
    <>
      <li className={pathname==="/create"? "active": ""}><Link to="/create"><span className="glyphicon glyphicon-pencil"></span> New Blog</Link></li>
      <li className={pathname==="/signout"? "active": ""} onClick={handleSignout}><Link to="/signout"><span className="glyphicon glyphicon-log-in"></span> Signout</Link></li>
    </>)
  } else {
      return <>
        <li className={pathname==="/signup"? "active": ""}><Link to="/signup"><span className="glyphicon glyphicon-user"></span>Signup</Link></li>
        <li className={pathname==="/signin"? "active": ""}><Link to="/signin"><span className="glyphicon glyphicon-log-in"></span> Signin</Link></li>
      </>
  }
}

function Navbar() {
  const {pathname} = useLocation()
  return (
    <nav className="navbar navbar-inverse">
      <div className="container-fluid">
        <div className="navbar-header">
          <Link className="navbar-brand" to="#">Fauna Blog</Link>
        </div>
        <ul style={{display:'inline'}} className="nav navbar-nav">
          <li className={pathname==="/"? "active": ""}><Link to="/">Home</Link></li>
          <li className={pathname==="/blogs"? "active": ""}><Link to="/blogs">Blogs</Link></li>
        </ul>
        <ul style={{display:'inline'}} className="nav navbar-nav navbar-right">
          <DynamicSignup isLoggedIn={localStorage.getItem('userId')? true: false} />
        </ul>
      </div>
    </nav>
  );
}

export default Navbar;
Enter fullscreen mode Exit fullscreen mode

SignOut component

In your signout component, type the following:

import { useHistory } from "react-router";

export default function Signout() {
  const history = useHistory()
  const handleClick = () => {
    localStorage.clear()
    history.push('/')
  }
  return (
    <div className="signin__input mt-6">
      <button onClick={handleClick}>Sign Out</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

I checked if the user details are stored in localstorage to know if the user is logged in in the Navbar component. If the user is logged in, there shouldn’t be a sign-in and sign-up button; instead, there should be a sign-out and new blog component.

Next, we’ll be building our sign-in and sign-up screens.

SignIn page

In your screens/Signin.js file, type the following:

import {useRef} from 'react'
import { useHistory } from 'react-router-dom';
import {loginUser} from '../models'

export default function SignIn() {
  let history = useHistory()
  if (localStorage.getItem('userId')) {
  history.push('/') 
  }
  const email = useRef('')
  const password = useRef('')

  const LoginUser = async (e) => {
    e.preventDefault()
    const body = {
      email: email.current.value,
      password: password.current.value
    }
    // Handle login logic
    if (!body.email || !body.password) {
      alert('You need to input an email and password')
    } else {
      const user = await loginUser(body.email, body.password)
      console.log(user)
      if (user) {
        localStorage.setItem('userId', user.id)
        localStorage.setItem('username', user.username)
        localStorage.setItem('email', user.email)
        history.push('/')
      } else {
        alert('Invalid email or password')
      }
    }
  }
  return (
    <form className="form-horizontal">
    <div className="form-group">
      <label className="control-label col-sm-4">Email address: </label>
      <input ref={email} type="email" className="form-control mx-md-3 col-sm-4" placeholder="Enter email" />
    </div>
    <div className="form-group">
      <label className="control-label col-sm-4">Password: </label>
      <input ref={password} type="password" className="form-control mx-md-3 col-sm-4" placeholder="Password" />
    </div>
    <div className="form-group">
        <div className="col-sm-5"></div>
        <button onClick={LoginUser}  type="submit" className="btn btn-primary col-sm-2">Signin</button>
      </div>
  </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

SignUp page

In our screens/signup.js file type the following:

import {useRef} from 'react'
import { createUser } from '../models';
import {useHistory} from 'react-router-dom'

export default function SignIn() {
  const history = useHistory()
  if (localStorage.getItem('user')) {
    history.push('/')
  }
  const name= useRef()
  const email = useRef()
  const password = useRef()
  const username = useRef()
  const confirm_password = useRef()
  const LoginUser = async (e) => {
    e.preventDefault()
    const body = {
      email: email.current.value,
      name: name.current.value,
      username: username.current.value,
      password: password.current.value
    }
    if (body.name && body.password && body.email && body.username && body.password === confirm_password.current.value) {
      const user = await createUser(body.name, body.email, body.username, body.password)
      if (!user) {
        alert('Email or username has been chosen')
      } else {
        localStorage.setItem('userId', user.id)
        localStorage.setItem('username', user.username)
        localStorage.setItem('email', user.email)
        history.push('/')
        alert('Account created sucessfully, signing you in...')
      }
    } else if (!name || !email || !username || !password) {
      alert('You didn\'t pass any value')
    } else {
      alert('Password and confirm password fields must be equal')
    }

    console.log(body)
  }

  return (
    <form className="form-horizontal">
      <div className="form-group">
        <label className="control-label col-sm-4">Name: </label>
        <input ref={name} type="text" className="form-control mx-md-3 col-sm-4" placeholder="Enter Name" />
      </div>
      <div className="form-group">
        <label className="control-label col-sm-4">Email address</label>
        <input ref={email} type="email" className="form-control mx-md-3 col-sm-4" placeholder="Enter email" />
      </div>
      <div className="form-group">
        <label className="control-label col-sm-4">Username: </label>
        <input ref={username} type="text" className="form-control mx-md-3 col-sm-4" placeholder="Enter username" />
      </div>
      <div className="form-group">
        <label className="control-label col-sm-4">Password</label>
        <input ref={password} type="password" className="form-control mx-md-3 col-sm-4"  placeholder="Password" />
      </div>
      <div className="form-group">
        <label className="control-label col-sm-4">Confirm Password</label>
        <input ref={confirm_password} type="password" className="form-control mx-md-3 col-sm-4" placeholder="Password" />
      </div>
      <div className="form-group">
        <div className="col-sm-5"></div>
        <button onClick={LoginUser}  type="submit" className="btn btn-primary col-sm-2">Signup</button>
      </div>
  </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

I ensured that the user inputs a username and password before clicking the sign-in component’s submit button. Also, in the signup button, I validated that the user inputs data in all input fields. I validated that the username and email haven’t been used in the data before. I was able to achieve this quickly because of the user_by_email and user_by_username indexes. After signing up and logging in, I stored some data to the localstorage, which was used to check if the user is authenticated. I used the useHistory() hook from react-router-dom to redirect the user back to the home page.

Blog Preview Component

Next Let’s create our BlogPreview component, in your components/BlogPreview.js file type the following:

import {Link} from 'react-router-dom'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import { faThumbsDown, faThumbsUp } from '@fortawesome/free-solid-svg-icons'


export default function BlogPreview({id, title, author, avatar, upvote, downvote}) {

  return (
    <div className="col-md-4 col-sm-6 card" style={{maxWidth: '380px', margin:'18px', marginLeft: '50px'}}>
      <img className="card-img-top" height="50%" src={avatar} alt=""/>
      <div className="card-body">
        <h5 className="card-title">{title}</h5>
        <p className="card-text">Post created by {author.username}</p>
        <div style={{margin: '5px'}}>
        <button onClick={() => {alert('View this blog to upvote it')}}>
            <FontAwesomeIcon icon={faThumbsUp} />
        </button> {upvote}
        <span style={{margin: "10px"}}></span>
        <button onClick={() => {alert('View this blog to downvote it')}}>
           <FontAwesomeIcon icon={faThumbsDown} />
        </button>{downvote}
      </div>
        <Link to={`/blogs/${id}`} className="btn btn-primary">Read blog</Link>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This component uses font awesome icons among a couple of things to display the blog. I used the Link component of react-router-dom to link each blog to their respective blog page, which we’ll create soon.

Home page component

In your screen/HomePage.js file type the following:

import { useEffect, useState } from 'react';
import BlogPreview from '../components/BlogPreview'
import {getPosts} from '../models'

export default function HomePage() {
  const [blogs, setBlogs] = useState([])
  useEffect(() => {
    async function fetchBlogs() {
      // You can await here
      let data = await getPosts()
      setBlogs(data)
    }
    fetchBlogs();
  }, [])
  return (
    <div className="">
        <hr/>
      <div className="row">
        {blogs.length > 0 ? blogs.map((blog, idx) => 
            <BlogPreview 
            key={idx}
            id={blog.ref.value.id}
            title={blog.data.title}
            author={blog.data.author}
            avatar={blog.data.avatar}
            upvote={blog.data.upvote}
            downvote={blog.data.downvote}/>
        ): 'No blog has been created yet. Be the first to create'}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In this screen, I used the useState hook of react js to store states. I also used the useEffect hook of react, which will help us render and rerender our DOM component. I also updated the state inside this hook. The [], which is the second parameter passed to useEffectmakes the hook work like componentDidMount, means that the code inside it will run only during the first render. I used the BlogPreview component inside this file, which is what we need to display.

Before updating our App.js file and running what we have, let’s create a 404 page and our single blog page.

404 page

In your screens/NotFound.js type the following:

import React from 'react'

export default function NotFound() {
  return (
    <div>
      <img  width="100%" height="550px" src="https://i2.wp.com/learn.onemonth.com/wp-content/uploads/2017/08/1-10.png?fit=845%2C503&ssl=1" alt=""/>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

This page will be displayed if we go to a page that isn’t defined in our app.

Single Blog Page

In your screens/Blog.js file, type the following:

import { useParams} from 'react-router-dom'
import {useEffect, useState} from 'react'
import {getPost, upvotePost, downvotePost} from '../models'
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import { faThumbsDown, faThumbsUp } from '@fortawesome/free-solid-svg-icons'

const Blog = () => {
  const {id} = useParams()
  const [blogData, setBlogData] = useState({})

  const handleUpvote = async e => {
    let blog = await upvotePost(blogData.upvote+1, id)
    setBlogData(blog)
  }

  const handleDownvote = async e => {
    let blog = await downvotePost(blogData.downvote+1, id)
    setBlogData(blog)
  }
  useEffect(() => {
    async function fetchBlog() {
      let data = await getPost(id)
      setBlogData(data)
    }
    fetchBlog();
  }, [id, blogData])
  return (
    <div>
      <img src={blogData.avatar} width="100%" height="400px" alt=""/>
      <h1>{blogData.title}</h1>
      <span className="text-muted">{blogData.author && `Post by ${blogData.author.username}`} on {blogData.created__at}</span>
      <hr/>
      <div dangerouslySetInnerHTML={{__html: blogData.body}}></div>
      <hr/>
      <div>
        <button 
          onClick={handleUpvote}>
            <FontAwesomeIcon icon={faThumbsUp} />
        </button> {blogData.upvote}
        <span style={{margin: "10px"}}></span>
        <button 
          onClick={handleDownvote}>
            <FontAwesomeIcon icon={faThumbsDown} />
        </button>{blogData.downvote}
      </div>
    </div>
  )
}

export default Blog
Enter fullscreen mode Exit fullscreen mode

This component uses the getPost function in the models.js file. I used the useParams hook of react-router-dom to get the id in the URL, and I passed the id in the getPost function to get the blog with the given id. The blog post is expected to have the following fields:

  1. title: Title of the blog
  2. body: The blog’s content contains HTML tags since we’ll use CKeditor to create a blog.
  3. avatar: Image URL of the blog. We’ll be storing the image itself in Cloudinary.
  4. upvote: Number of upvotes a blog has.
  5. downvote: Number of downvotes a blog has.
  6. author: This is a JSON object which contains the details of the author. It contains name, email, and username.

App component

In your App.js file, type the following:

import { BrowserRouter as Router, Switch, Route } from "react-router-dom";
import HomePage from './screens/HomePage'
import SignIn from './screens/SignIn'
import SignUp from './screens/SignUp'
import NotFound from './screens/NotFound'
import Blog from './screens/Blog'
import Navbar from "./components/Navbar"

function App() {
  return (
    <Router>
      <Navbar />
      <Switch>
        <Route exact path="/" component={HomePage} />
        <Route exact path="/blogs/" component={HomePage} />
        <Route path="/blogs/:id/" component={Blog} />
        <Route exact path="/signin/" component={SignIn} />
        <Route exact path="/signup/" component={SignUp} />
        <Route exact path="*" component={NotFound} />
      </Switch>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

This component is where our application is created. I used React Router. I used Switch, BrowserRouter, and Route to add URL endpoints for components. The Route component is used to create an endpoint for a component. The exact parameter means that the component will match the endpoint with the same URL. The Route component has to be in the Switch component, which means that only one of the components should be displayed at a time. The switch component is inside the BrowserRouter component. I added the Navbar. The component above the Route component, this way, I don’t have to add the Navbar component in all the files in the screens folder.

Testing Our App

Let’s test what we have so far. The create-react-app CLI tool we used to bootstrap our app created some scripts in our package.json file. We need the start command, which runs our app in development mode.

In the terminal, type the following:

npm start
Enter fullscreen mode Exit fullscreen mode

The above command starts the server and opens the app in the browser, and you should see the following:

Alt Text

Click the buttons in the navbar, and you should notice that the URL is changing; this is because of the components we defined in our Routes components in our App.js file.

Alt Text

Alt Text

Alt Text

Alt Text

Test the signup and sign-in pages’ functionality by creating an account, then log out and log in again. If you click the new blogs button while signed, you should see a 404 page; this is because we haven’t defined our create blog component and added a route for it.

Creating a new blog

To create a new blog, I used the react library for CKeditor, which I used for the blog creation. You can always use Markdown or any other text editor. You can check out react-markdown if you’ll be using markdown to reproduce your own. Also, I used Cloudinary to upload images. In this case, the only picture uploaded in the blog is the blog’s avatar.

In your screens/CreateBlog.js file type the following:

import {useState, useRef} from 'react'
import {createPost} from '../models'
import { CKEditor } from '@ckeditor/ckeditor5-react';
import ClassicEditor from '@ckeditor/ckeditor5-build-classic';
import {useHistory} from 'react-router-dom'
import axios from 'axios';
import {config} from 'dotenv'

config()

export default function CreateBlog() {
  const history = useHistory()
  if (!localStorage.getItem('userId')) {
    alert('You need to be logged in to create a blog!')
    history.push('/')
  }
  const [content, setContent] = useState('<h2>Body of your article goes here...</h2>')
  const tags = useRef('')
  const title = useRef('')
  const avatar = useRef('')


  const handleCreate = async (e) => {
    e.preventDefault()
    if (!title.current.value || !tags.current.value || !avatar.current.value) {
      alert('You need to add title, body and upload the avatar')
    } else {
      const url = await uploadFile(avatar.current.files[0])
      await createPost(title.current.value, content, url, localStorage.getItem('userId'), tags.current.value.split(','))
      alert('Blog post created successfully, signing you in...')
      history.push('/')
    }
  }

  return (
    <form className="form-horizontal">
      <div className="form-group files">
        <label className="control-label col-sm-4" htmlFor="upload">Upload avatar</label>
        <input type="file" className="form-control mx-md-3 col-sm-4" id="" ref={avatar}/>
      </div>
      <div className="form-group">
        <label className="control-label col-sm-4" htmlFor="title">Title</label>
        <input className="form-control mx-md-3 col-sm-4" ref={title} type="text" name="title" id=""/>
      </div>
      <div>
        <label className="control-label col-sm-4" htmlFor="tags">Tags</label>
        <input className="form-control mx-md-3 col-sm-4" ref={tags} type="text"  />
        <div className="col-sm-4"></div>
      </div>
      <br/><br/><br/>
      <div className="form-group">
        <CKEditor
          editor={ ClassicEditor }
          data={content}
          row={100}
          onReady={ editor => { } }
          onChange={ ( event, editor ) => {
              const data = editor.getData();
              setContent(data)
          } }
        />
    </div>
    <div className="form-group">
        <div className="col-sm-5"></div>
        <button onClick={handleCreate}  type="submit" className="btn btn-primary col-sm-2">Submit</button>
      </div>
    </form>
  )
}


const uploadFile = async (file) => {
  const url = `https://api.cloudinary.com/v1_1/${process.env.REACT_APP_CLOUD_NAME}/image/upload`;
  const timeStamp = Date.now()/1000;
  let formData = new FormData()
  formData.append("api_key",process.env.REACT_APP_CLOUDINARY_API_KEY);
  formData.append("file", file);
  formData.append("public_id", "sample_image");
  formData.append("timestamp", timeStamp);
  formData.append("upload_preset", process.env.REACT_APP_PRESET);
  let respData = await axios.post(url, formData)
  return respData.data.secure_url
}
Enter fullscreen mode Exit fullscreen mode

As you might have noticed, I used three extra environmental variables in this component which I got from my Cloudinary dashboard. You can get your cloud name and API from your Cloudinary dashboard. The preset created for us by default can’t be used in an application, so we need to create a new one that has to be whitelisted anywhere. To do so, click on the Settings Icon in your dashboard, then Upload. Scroll down to the upload presets section and create a new one, ensure you change the signing mode to unsigned.

Add the following to your .env file:

REACT_APP_PRESET='your preset'
REACT_APP_CLOUD_NAME='your cloud name.'
REACT_APP_CLOUDINARY_API_KEY='your API key.'
Enter fullscreen mode Exit fullscreen mode

Additionally, I used the CKeditor components to create a text box for writing the blogs’ content.

In your App.js file, add the following just after the last import statement

import CreateBlog from "./screens/CreateBlog";
Enter fullscreen mode Exit fullscreen mode

Also, add the following just before where we declared the route for 404 pages,

<Route exact path="/create/" component={CreateBlog} />
Enter fullscreen mode Exit fullscreen mode

Alt Text

Create a couple of blogs, and now if you go to the home or blog page, you should be able to see something similar to the following.

Alt Text

Next, click on a single blog. You should be able to see something similar to the image below.

Alt Text

Alt Text

The upvote and downvote buttons are also working perfectly. You can click on the upvote and downvote buttons, and you’d notice that the DOM gets updated, and it also updates the data in the fauna database.

Conclusion

This article has walked you through how to build a fully functional blog app with React JS, FaunaDB, CKEditor, and Cloudinary. You can access the code snippet for this app here and the deployed version of the app is here. Should you have any issues, you can contact me via Twitter. Additionally, you can create a profile page so users can easily update their profile, view the blogs they created and as you have seen, the UI of the app isn't good enough, that is because the goal of the tutorial isn't to teach CSS, you can always change the UI of the app.

Oldest comments (4)

Collapse
 
watchdogtimer profile image
Tom Hartwell

This was a great post, thanks for posting. It helped me get started with FaunaDB. However, your use of FaunaDB leaves the secret out in the open. For example, I could use your secret to work with all of your data in FaunaDB. I recommend taking down your live app since it can be "hacked".

Collapse
 
bkoiki950 profile image
Babatunde Koiki

Haha! thanks man

Collapse
 
bkoiki950 profile image
Babatunde Koiki

It's actually a test app, I only built it to demonstrate this tutorial.
Thanks btw

Collapse
 
0xchance profile image
Chance Dare • Edited

For people who chose the US region(I assume EU aswell), you have to use a different domain when instantiating the client. For example

const client = new faunadb.Client({
secret: process.env.REACT_APP_FAUNA_KEY,
domain: 'db.us.fauna.com',
})

If you do not, you will get unauthorized errors!