DEV Community

loading...
Cover image for How to build a full-stack serverless movie tracker with OpenJS Architect - Part 2

How to build a full-stack serverless movie tracker with OpenJS Architect - Part 2

Paul Chin Jr.
Curious Human. #Serverless #JavaScript Dev @ http://arc.codes; Dev Rel at http://begin.com; Prophet of Nicolas Cage; #PraiseCage
・3 min read

Progressive Enhancement and delivering client-side JS

The basic functionality is completed. Now it's time to enhance this form with some JavaScript so it saves the watched checkboxes in real-time by submitting the form when a checkbox is clicked.

// public/index.js
let forms = document.querySelectorAll("form[action='/watched']")

for (let f of forms) {
  f.querySelector('button').style.display = 'none'
  let check = f.querySelector('input[type="checkbox"]')  
  check.addEventListener('change', function (e) {
    f.submit() 
  }, false)
}
Enter fullscreen mode Exit fullscreen mode

This is the simplest version of progressive enhancement we can make. The client-side JavavScript selects the forms, hides the 'save' button and adds an event listener to the checkbox. Each time the checkbox changes, it submits the form for the user.

Adding star ratings and comments

We're going to continue making our application more complex. In the next few sections, we will add a "star rating" feature as well as a comment system.

Alt Text

First, lets change our markup in the get-index route.

// src/http/get-index/index.js

const arc = require('@architect/functions')
const data = require('@begin/data')

exports.handler = arc.http.async(http)

function authControl(account) {
  if (account && account.name) {
    return `
    Welcome back ${account.name}
    <form action=/logout method="post">
    <button>Logout</button>
    </form>`
  } else {
    let clientID = process.env.GITHUB_CLIENT_ID
    let redirectURL = process.env.GITHUB_REDIRECT
    let href = `https://github.com/login/oauth/authorize?client_id=${clientID}&redirect_url=${redirectURL}`
    return `
    <a href='${href}'>Login with GitHub to see a list of movies</a>
    `
  }
}

function movie({ key, watched, title, rating, review }) {

  return `
<form action="/watched" method="post">
  <input type="hidden" name="movieId" value="${key}">
  <input type="checkbox" data-movieid="${key}" name=watched ${watched ? 'checked' : ''}>
  ${title}
  <input type="text" data-movieid="${key}" name="review" placeholder="leave a review here" value="${review || ''}">

  <input type="radio" name="rating" data-movieid="${key}" value="1" ${rating === '1' ? 'checked' : ''}>
  <input type="radio" name="rating" data-movieid="${key}" value="2" ${rating === '2' ? 'checked' : ''}>
  <input type="radio" name="rating" data-movieid="${key}" value="3" ${rating === '3' ? 'checked' : ''}>

  <button class=cage>Save</button>
  </form>`
}

async function getMovies(account) {
  let movies = [
    { key: '001', title: 'Raising Arizona' },
    { key: '002', title: 'Con Air' },
    { key: '003', title: 'National Treasure' },
  ]
  if (account) {
    let accountMovies = await data.get({
      table: `${account.id}-movies`
    })

    let result = ''
    for (let mov of movies) {
      let found = (accountMovies.find(m => m.key === mov.key))
      result += movie({
        key: mov.key,
        title: mov.title,
        watched: !!found,
        rating: found ? found.rating : '',
        review: found ? found.review : ''
      })
    }
    return result
  }
  return ''
}

async function http(req) {

  return {
    html: `
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="/_static/index.css">
  <link rel="shortcut icon" href="#">
  <title>Praise Cage</title>
</head>
<body>
<h1>Praise Cage</h1>

${authControl(req.session.account)}
${await getMovies(req.session.account)}

<script src=/_static/index.js type=module></script>
</body>
</html>
`
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, we can modify the post-watched endpoint. We now want to include review and rating properties to be saved to the database.

// src/http/post-watched/index.js

const arc = require('@architect/functions')
const data = require('@begin/data')

exports.handler = arc.http.async(route)

async function route(req) {

  console.log('post-watched req.body:', req.body )

  let account = req.session.account.id

  if (req.body.watched) {
    await data.set({
      table: `${account}-movies`,
      key: req.body.movieId,
      review: req.body.review,
      rating: req.body.rating
    })
  } else {
    await data.destroy({
      table: `${account}-movies`,
      key: req.body.movieId
    })
  }

  return {
    location: '/'
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we can update our client-side JS with the following:

// public/index.js

let forms = document.querySelectorAll("form[action='/watched']")

console.log(forms)

for (let f of forms) {

  // hide all submit buttons
  f.querySelector('button').style.display = 'none'

  // get a ref to the form checkbox
  let check = f.querySelector('input[type="checkbox"]')

  // get a ref to the form radios
  let radios = f.querySelectorAll('input[type="radio"]')

  // get a ref to the form text
  let text = f.querySelector('input[type="text"]')

  // mutates state
  function changed (e) {

    let movieId = e.target.dataset.movieid
    let payload = { movieId }
    payload.watched = f.querySelector('input[name="watched"]').checked
    payload.review = f.querySelectorAll('input[name="review"]')[0].value

    let rating = f.querySelectorAll('input[name="rating"]:checked')
    payload.rating = rating.length === 1 ? rating[0].value : ''

   //make an HTTP post with fetch

    fetch('/watched', {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        'x-nick-cage': 'fetch'
      },
      body: JSON.stringify(payload)
    }).catch(function fail(err) {
      console.log('failed', err)
    })
  }

  // listen to checkbox changes
  check.addEventListener('change', changed, false)

  // listen to radio buttons getting hit
  for (let r of radios) {
    r.addEventListener('input', changed, false)
  }

  // listen to changes to review text
  text.addEventListener('input', changed, false)
}
Enter fullscreen mode Exit fullscreen mode

The full source code can be found here: https://github.com/pchinjr/movie-tracker

Discussion (0)

Forem Open with the Forem app