October 12, 2019 - Projects

Adding comments to a Gatsby blog using Contentful Pt.2

Continuing on from our previous post where we retrieved our comments, next we are going to build in functionality to add comments.

Wall with text displaying the words I love you in various ways.

The last post was pretty straight forward, nothing too complicated I feel. This post will bit a little more complex but still should be straight forward.

Continuing on, we should have our comments stored in state. In order to display our comments I built a comments component which takes an array of comments and displays them nicely. You can see the code outlined below:

import React from "react"
import { formatDate } from "../util/utilityFunctions"
const comments = props => {
  const order = props.order === true ? "Oldest First" : "Most Recent"
  return (
    <>
      <div className={styles.details}>
        {props.comments && <span>{props.comments.length} Comments</span>}
        <button onClick={props.sortComments}>{order}</button>
      </div>
      {props.comments &&
        props.comments.map(comment => (
          <div className={styles.commentContainer} key={comment.id}>
            <div className={styles.comment}>
              <div className={styles.commentDetails}>
                <span className={styles.commentDetailsName}>
                  <a href={comment.handle}>{comment.name}</a>
                </span>
                <span className={styles.commentDetailsDate}>
                  {formatDate(comment.timestamp)}
                </span>
              </div>
              <p>{comment.message}</p>
              <button
                className={styles.commentReplyBtn}
                onClick={() => props.reply(comment.id, comment.name)}
              >
                Reply
              </button>
            </div>
            <div className={styles.replies}>
              {comment.replies.map((comment, index) => (
                <div className={styles.commentReply} key={index}>
                  <div className={styles.commentDetails}>
                    <span className={styles.commentDetailsName}>
                      <a href={comment.handle}>{comment.name}</a>
                    </span>
                    <span className={styles.commentDetailsDate}>
                      {formatDate(comment.timestamp)}
                    </span>
                  </div>
                  <p>{comment.message}</p>
                </div>
              ))}
            </div>
          </div>
        ))}
    </>
  )
}
export default comments

Lets run through this code briefly and explain the important parts:

  1. The order variable declared at the top is used for the display text in the sorting functionality.

  2. A simple span element displaying the number of comments.

  3. We map over the comments if they exist and display them, adding a reply button which is linked to a function that takes the post ID and name of person who made the comment. ( we use name as a placeholder in the textarea input.

Now that we have our comments displayed nicely, we should work on the functionality to add comments. First we can add a form with the required inputs as follows:

{
  this.props.data.contentfulBlogPost.enableComments && (
    <>
      <Comments
        comments={this.state.comments}
        reply={this.reply}
        sortComments={this.sortComments}
        order={this.state.sorting}
      />
      <form className={styles.form} onSubmit={this.addComment}>
        Leave a comment
        <label htmlFor="name">Name:</label>
        <input
          type="text"
          id="name"
          name="name"
          value={this.state.name}
          onChange={this.handleChange}
          required
        />
        <label htmlFor="handle">Handle:</label>
        <input
          type="text"
          id="handle"
          name="handle"
          onChange={this.handleChange}
          placeholder="Will be linked in your name (optional)"
        />
        <label htmlFor="message">
          <span> Message:</span>
          <button
            onClick={evt => this.canceReply(evt)}
            className={styles.cancelReplyBtn}
          >
            {this.state.reply ? "Cancel Reply" : ""}
          </button>
        </label>
        <textarea
          name="message"
          id="message"
          cols="30"
          rows="10"
          value={this.state.message}
          onChange={this.handleChange}
          placeholder={this.state.reply ? this.state.replyName : ""}
          required
        />
        <input
          type="submit"
          value={this.state.loading ? "Adding..." : "Add Comment"}
        />
        <span>{this.state.msg}</span>
      </form>
    </>
  )
}

Lets run through this code briefly and explain the important parts:

  1. If enable comments is set to true we will display the form and comments component.

  2. In the comments component we pass down the comments array, two functions; one for replies and one for sorting comments and also an order prop which is a boolean for which we explained above.

  3. The form itself is pretty standard. We give it an onSubmit function and also a cancelReply function whose function is to simply cancel any reply you may have started.

Heres the code for the functions we just mentioned:

Sort Comments

sortComments = () => {
  const comments = this.state.comments
  if (!this.state.sorting) {
    const sorted = comments.sort((a, b) => (a.timestamp < b.timestamp ? -1 : 1))
    this.setState({
      comments: sorted,
      sorting: true
    })
  } else {
    const sorted = comments.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1))
    this.setState({
      comments: sorted,
      sorting: false
    })
  }
}

Reply

reply = (id, name) => {
  window.scrollTo(50, document.body.scrollHeight)
  this.setState({
    reply: true,
    replyName: `Replying to ${name} `,
    replyID: id
  })
}

Cancel Reply

canceReply = evt => {
  evt.preventDefault()
  this.setState({
    reply: false,
    replyName: ``,
    replyID: null
  })
}

Handle Change


handleChange = evt => {
  evt.persist()
  this.setState({
    [evt.target.name]: evt.target.value
  })
}

Before we show the Add Comment function, I want to show you the Netlify function, doing so will make it easier to understand.

"use strict"
const contentful = require("contentful-management")
const util = require("../src/util/utilityFunctions")
exports.handler = function(event, context, callback) {
  async function main() {
    // Setup variables
    const data = JSON.parse(event.body)
    const { name, handle, message, ID, reply, replyID } = data
    let postComments = []
    // Connect to contentful
    const client = contentful.createClient({
      accessToken: "XXXX-XXXX-XXXX"
    })
    // Get the entry based on post ID.
    await client
      .getSpace("XXXXXXXXX")
      .then(space => space.getEnvironment("master"))
      .then(environment => environment.getEntry(ID))
      .then(entry => {
        // Get current comments
        entry.fields.comments["en-US"].comments.forEach(comment => {
          postComments.push(comment)
        })
        if (reply) {
          postComments.forEach(comment => {
            if (comment.id === replyID) {
              comment.replies.push({
                name: name,
                handle: handle,
                message: message,
                timestamp: Math.round(new Date().getTime() / 1000),
                id: util.uuidv4()
              })
            }
          })
        } else {
          postComments.push({
            name: name,
            handle: handle,
            message: message,
            timestamp: Math.round(new Date().getTime() / 1000),
            id: util.uuidv4(),
            replies: []
          })
        }
      })
    await client
      .getSpace("XXXXXXXXX")
      .then(space => space.getEnvironment("master"))
      .then(environment => environment.getEntry(ID))
      .then(entry => {
        // Update comments
        entry.fields.comments = { "en-US": { comments: postComments } }
        // Update post
        return entry.update()
      })
      .catch(console.error)
    // Callback with updated comments to update state
    callback(null, {
      statusCode: 200,
      body: JSON.stringify({
        comments: postComments
      })
    })
  }
  main().catch(console.error)
}

Lets break this down:

  1. You'll notice a lot of similar code to the other Netlify function for getting comments.

  2. We setup our variables from the POST request including name, handle, message, ID, reply, replyID.

    • Name, Handle Message are self-explanatory.

    • ID is for getting the correct post on Contentful.

    • reply is a boolean to tell the Netlify function if this is a reply or not.

    • replyID is the original comments ID used to add the reply to the right comment.

  3. Get the correct entry based on the ID and store the comments in a variable

  4. If reply is equal to true we loop over the comments and match the comment ID with the reply ID.

  5. Once found, we push the comment into the replies array of that comment.

  6. If it's not a reply we just add the comment to the comments array itself.

  7. We create a uuid and timestamp to go along with it either way. ( The timestamp isn't completely accurate but thats ok for our purposes here )

  8. We callback with the updated comments to update state to keep everything fresh.

And now the Add Comment function...

addComment = evt => {
  evt.preventDefault()
  const form = evt.target
  const axiosConfig = {
    header: {
      Accept: "application/json",
      "Content-Type": "application/json",
      "Access-Control-Allow-Origin": "*"
    }
  }
  // Check if a comment has been made by the current user.
  const localTimestamp = localStorage.getItem("timestamp")
  if (localTimestamp) {
    // Check if comment was made less than 60 seconds ago.
    const date1 = new Date(localTimestamp * 1000)
    const date2 = new Date(Math.round(new Date().getTime() / 1000) * 1000)
    const res = Math.abs(date1 - date2) / 1000
    var minutes = Math.floor(res / 60) % 60
    var seconds = res % 60
    // If not, go ahead and comment.
    if (minutes >= 1) {
      this.setState({
        loading: true,
        timestamp: Math.round(new Date().getTime() / 1000)
      })
      axios
        .post(
          `/.netlify/functions/<your-function-name>`,
          {
            name: this.state.name,
            handle: this.state.handle,
            message: this.state.message,
            ID: this.props.data.contentfulBlogPost.contentful_id,
            reply: this.state.reply,
            replyID: this.state.replyID
          },
          axiosConfig
        )
        .then(response => {
          if (response.status === 200) {
            this.setState({
              comments: response.data.comments
            })
            form.reset()
            this.setState({
              reply: false,
              replyName: "",
              replyID: null,
              name: "",
              handle: "",
              message: "",
              timestamp: "",
              loading: false,
              msg: ""
            })
          } else {
            this.setState({
              msg: "Opps, something went wrong. Please try again"
            })
          }
        })
      // Update the timestamp to reflect new comment
      const timestamp = Math.round(new Date().getTime() / 1000)
      localStorage.setItem("timestamp", timestamp)
      // If the last comment is under 60 seconds, notify user.
    } else if (minutes < 1) {
      this.setState({
        msg: `You're commenting too fast! Please try again in ${60 -
          seconds} seconds`
      })
    }
    // If there was no stored timestamp from the current user, set the timestamp and make the comment
  } else {
    const timestamp = Math.round(new Date().getTime() / 1000)
    localStorage.setItem("timestamp", timestamp)
    axios
      .post(
        `/.netlify/functions/addComment`,
        {
          name: this.state.name,
          handle: this.state.handle,
          message: this.state.message,
          ID: this.props.data.contentfulBlogPost.contentful_id,
          reply: this.state.reply,
          replyID: this.state.replyID
        },
        axiosConfig
      )
      .then(response => {
        if (response.status === 200) {
          this.setState({
            comments: response.data.comments
          })
          form.reset()
          this.setState({
            reply: false,
            replyName: "",
            replyID: null,
            name: "",
            handle: "",
            message: "",
            timestamp: "",
            loading: false,
            msg: ""
          })
        } else {
          this.setState({
            msg: "Opps, something went wrong. Please try again"
          })
        }
      })
  }
  setTimeout(() => {
    this.setState({ msg: "" })
  }, 5000)
}

Whew... Thats a long function.. And it's a little more complicated as you can see. Lets break it down to make it a bit easier to understand.

  1. Some setup with https headers for the POST request.

  2. We check local storage for a timestamp which will tells us if and when the user made a comment last.

  3. If there is we compare it to the current timestamp to see if its within the last 60 seconds or not. ( we do this to avoid spamming the comments ) If it's been more than a minute we go ahead and make the comment.

  4. We make a POST request with the required info and reset info after it has been successful.

  5. If the user hasn't made a comment and therefore, isn't any timestamp we create a timestamp in local storage and make the comment.

  6. We also receive a response from the POST request that we use to update state to keep everything fresh and up to date.

Once we have implemented all this, we should have a functioning comment system. Obviously its not a complete step by step guide from scratch but this is all you need to get started.

There's a bunch of things you could do to improve this some of which are but not limiting to these:

  • Adding extra validation such as a reCAPTCHA

  • Adding a backend to password protected screens where you can edit/remove/approve all the comments yourself.

  • Sending notifications by email when a new comment comes in.

  • Extra level of replies.

That's it! If you see anything I could improve on or you get stuck somewhere please do leave a comment.

0 Comments
Leave a comment