DEV Community

Kiana
Kiana

Posted on

Javascript and Rails API!

For my fourth project with Flatiron I was tasked with building
a Single Page Application. A javascript/CSS/HTML frontend that will communicate with a backend API built with Ruby and Rails.

I work as a Nanny and the kids favorite game is a memory matching card game! we spend our time drawing small matching pictures, cutting them into card sizes, flipping them picture side down and timing each other with how fast we can flip them all over.
I knew pretty quickly creating a memory match game for my project would be a good way to meet all my requirements and challenge myself with building a game.

I brainstormed the backend would house all my stored data for a user and their score through a gameplay. I assumed I would just display the top 3 scores but I also liked the idea of displaying users and their best score(I don't want to be the reason for a 5 and 6 year old fighting because their name keeps getting bumped off the board...)

While the front end Javascript would encapsulate related data and behavior, HTML/CSS would be used to created the cards and buttons.
I have really enjoyed learning all the ways to use CSS and wanted to challenge my knowledge and skills and implement some CSS animation on the cards which I knew I would be creating with stacking DIVs. I have been so impressed with all the hover, flip and unique features css can bring to a project.

I started this project with setting up 2 repos on git hub.
One for my frontend and one for my backend.

I went through all the usual steps of creating a rails app.
Backend (Ruby on Rails):

-Create models and relationships
-Create the database tables and seed
-Create controllers
-Run Rails s for starting the server

Frontend (JavaScript):

-setting up index.html
-index.js
-css file.

I created the basic layout of the cards which were stacked divs that I applied photos to each and would apply a front and back class I could use in css(and later in my index.js) to orient the cards how I wanted.

index.html


<div class="board">

            <div class="card">
                <div class=" front facing"> 
                    <img class="face" src="css/images/apple.png">
                </div>
                <div class="back facing">
                     <img class="brain" src="css/images/brain.png">
                </div>
            </div>
Enter fullscreen mode Exit fullscreen mode

I repeated this for each card making sure each photo had two for a match.

css (only relevant code shown)


.board {
  display: grid;
  grid-template-columns: repeat(4, auto);
  grid-gap: 10px;
  margin: 5px;
  perspective: 500px;
}

.card {
  position: relative;
  width: 120px;
  height: 125px;

}
.facing {
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  overflow: hidden;
  backface-visibility: hidden;
  border-radius: 12px;
  border-width: 1px;
  transition: transform 500ms ease-in-out;
}

.card.visible .back {
  transform: rotateY(-180deg);
}

.card.visible .front {
  transform: rotateY(0);
}
.back {
  position: absolute;
  height: 100%;
  width: 100%;
}
.brain {
  align-self: flex-start;
  transform: translateY(-10px);
  transition: transform 100ms ease-in-out;
  display: center;
  position: absolute;
  width: 108px;
  height: 108px;
  overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode

In my index.js I added an event listener to be able to flip a card when clicked!

let cards = Array.from(document.getElementsByClassName("card"));

let game = new Match(150, cards);

   cards.forEach(card => {
        card.addEventListener('click',() => {
            game.flipCard(card);
        });
    });
Enter fullscreen mode Exit fullscreen mode

I created a Match class to start with some of the basic logic for flipping cards based on if they were a match. I used the class name of "face" to compare if they were a matching photo and then by removing the visible classList to show the front of the card rather than the back if they were. This all together made it seem as though the card was being flipped!
I loved the challenge of using css and javascript together in this as one smooth event for a user.

class Match {
constructor(cards){
        this.cardsArray = cards;
}

...
      getCard(card) {
        return card.getElementsByClassName('face')[0].src;
    }

   flipCard(card) {
        if(this.canFlipCard(card)){
            card.classList.add('visible');
            if(this.cardToCheck)
            this.checkForCardMatch(card);
            else
            this.cardToCheck = card;  
        }
    }

      checkForCardMatch(card) {
        if(this.getCard(card) === this.getCard(this.cardToCheck))
            this.cardMatch(card, this.cardToCheck);
        else 
            this.cardMisMatch(card, this.cardToCheck);

        this.cardToCheck = null;
    }
     cardMatch(card1, card2) {
        this.matchedCards.push(card1);
        this.matchedCards.push(card2);
        if(this.matchedCards.length === this.cardsArray.length)
            this.win(); 
    }
       cardMisMatch(card1, card2) {
        this.busy = true;
        setTimeout(() => {
            card1.classList.remove('visible');
            card2.classList.remove('visible');
            this.busy = false;
        }, 1000);
    }

}

Enter fullscreen mode Exit fullscreen mode

To make all of this make sense I needed to check if a card was
even able to be flipped.

  canFlipCard(card){
     return !this.busy && !this.matchedCards.includes(card) && card !== this.cardToCheck
    }
Enter fullscreen mode Exit fullscreen mode

These 3 conditions are what needed to be meet in order to allow a card to be flipped(clicked).

If a card was not busy(ex. a click event... a user already clicked that card)
if the card clicked is not already a matched card
and if the card is not the current card being checked.

The return of all these conditions would be a boolean value of true or false... If all 3 of those conditions are false the value of canFlipCard would return true and the card would be able to be clicked and therefore flipped!

In the startGame function is where the card to check starts as null because nothing has been clicked yet and also set matchedCards to an empty array so they could be easily compared and stored for the scoring logic.

startGame(){
        this.cardToCheck = null;
        this.matchedCards = []
        this.hideCards();
    }
Enter fullscreen mode Exit fullscreen mode

From there I created the scoring logic for the flip count,
and timer counting down.

index.js


constructor(totalTime, cards){
        this.cardsArray = cards;
        this.totalTime = totalTime;
        this.timeRemaining = totalTime;
        this.timer = document.getElementById('timer');
        this.ticker = document.getElementById('flips');
    }
 startGame()
        this.cardToCheck = null;
        this.totalClicks = 0;
        this.timeRemaining = this.totalTime;
        this.matchedCards = [];
        this.busy = true;
        setTimeout(() => {
            this.countDown = this.startCountDown();
            this.busy = false;
        }, 500);
        this.hideCards();
        this.timer.innerText = this.timeRemaining;
        this.ticker.innerText = this.totalClicks;
    }
   hideCards(){
         this.cardsArray.forEach(card => {
             card.classList.remove('visible');
         });
     }
     startCountDown(){

         return setInterval(() => {
             this.timeRemaining--;
             this.timer.innerText = this.timeRemaining;
             if
             (this.timeRemaining === 0)
             this.gameOver();
         }, 1000);

     }
Enter fullscreen mode Exit fullscreen mode
index.html

<div class="info">
            Timer: <span id="timer">150</span>
 </div>
        <div class="info">
            Flips: <span id="flips">0</span>
        </div
Enter fullscreen mode Exit fullscreen mode

From here, in the API I created some data to fetch and display a user and their scores. In order to get all of this is display properly in the frontend I had to add the render json line so the data could be readable.

class UsersController < ApplicationController
def index
    @users = User.all
    render json: @users
end


class GamesController < ApplicationController
def index
    @games = Game.all
    render json: @games
end
end

seed.rb
User.create(username: "KJL")
User.create(username: "Kiana")
User.create(username: "K2")

Game.create(user_id: 1, score: 23, time: 67)
Game.create(user_id: 2, score: 14, time: 45)
Game.create(user_id: 3, score: 19, time: 84)
Enter fullscreen mode Exit fullscreen mode

and in the frontend,

api.js

   function callUsers(){
    return fetch('http://127.0.0.1:3000/users')
    .then(res => res.json())
    .then(json => renderUsers(json))
}

  function  callGames() {
  return fetch('http://127.0.0.1:3000/games')
    .then(resp => resp.json())
    .then(json => renderGames(json))

}
function renderUsers(users){
 const usersshow = document.getElementById("user-display")
  users.forEach(user => {
    const h2 = document.createElement('h2')
    h2.innerHTML = user.username
    usersshow.appendChild(h2)
  })
}

index.html

 <h1 class="info">Highscores</h1>
            <div id="user-display"> </div>
Enter fullscreen mode Exit fullscreen mode

I later decided to add a "login to play" form.
This would have a user create a username, once they submit it, the game would start. The score would be stored in localstorage and if a user completes the game without time running out they it would be persisted to the database.

index.html

   <h2 class="rules">Submit a username to play!</h2>
            <div class="layout" id="login-form">
                <form autocomplete="off" id="username" class="layout" action="http://127.0.0.1:3000/users" method="post">
                    <input class="rules" id="login-field" type="text" name="username" value="" placeholder="username">
                    <br><br>
                    <input class="startOver" type="submit" value="Lets Play!">
                    <br><br>
                </form>

            </div>

index.js
let loginForm = (document.getElementById("login-form"))

loginForm.addEventListener('submit',e => {
        e.preventDefault()
      let user = player.value
      let body = {username: user}
         createUser(body)
          if(!body.null)
           { overlays.forEach(overlay =>
       overlay.classList.remove('visible'));
       game.startGame()}
       else {
        alert("please enter a username")
       }
        }
        )


api.js
 function createUser(username){
        return fetch('http://127.0.0.1:3000/users',{
        method: 'POST',
        headers: {
         "Content-Type": "application/json",
         "Accept": "application/json"
         },
        body: JSON.stringify(username)
    })
}
   function  createGame(result){
    return fetch('http://127.0.0.1:3000/games',{
        method: 'POST',
        headers: {
         "Content-Type": "application/json",
         "Accept": "application/json"},
        body: JSON.stringify(result)
    })
}

index.js

    win() {
            localStorage.setItem("score", this.totalClicks)
            localStorage.setItem("time", this.timeRemaining)
            document.getElementById('win-text').classList.add('visible')
            let flipResult = localStorage.getItem("score")     
            let timeResult = localStorage.getItem("time") 
            const result = {score: flipResult, time: timeResult}
                 createGame(result)
            document.getElementById("flip-form").innerHTML = flipResult
            document.getElementById("time-form").innerHTML = timeResult

        }
Enter fullscreen mode Exit fullscreen mode

And in the backend updated the controllers, routes and models

controllers

class UsersController < ApplicationController
def create 
    @user = User.find_or_create_by(username: params[:username])
end


  private

  def user_params
    params.require(:username).permit(:id)
  end
end

class GamesController < ApplicationController
def create
    @user = User.find_by(username: params[:username])
    Game.create(score: params[:score], time: params[:time])
  end
end

models

class User < ApplicationRecord
      has_many :games
end
class Game < ApplicationRecord
    belongs_to :user
end

routes.rb

Rails.application.routes.draw do
  resources :games, only: [:index, :create]

  resources :users, only: [:index, :show, :create]

end
Enter fullscreen mode Exit fullscreen mode

I followed this same pattern to add a "leave a review" feature at the end of the game, these reviews were displayed in the game play window.

index.html

<h3 class="rules"> Leave a review! </h3>
            <form autocomplete="off" id="comment-form" class="layout" action="index.html" method="post">
                <input class="rules" id="user-field" type="text" name="" value="" placeholder="username">
                <br><br>
                <input class="rules" id="comment-field" type="text" name="" value="" placeholder="comment">
                <br><br>
                <input class="startOver" type="submit" value="submit">
                <br><br>
            </form>


api.js

    function  createComment(rrr, rrrr){
    return fetch('http://127.0.0.1:3000/comments',{
        method: 'POST',
        headers: {
         "Content-Type": "application/json",
         "Accept": "application/json"},
        body: JSON.stringify(rrr, rrrr)
    })
    .then(res => res.json())
}

 function renderComments(comments){
 const reviews = document.getElementById("comments")
//  const userreviews = document.getElementById("user-comments")
  comments.forEach(comment => {
    const h2 = document.createElement('h2')
    const h3 = document.createElement('h3')
    h2.innerHTML = comment.comment
    h3.innerHTML = comment.username
    reviews.appendChild(h2)
    reviews.appendChild(h3)

  })
}
Enter fullscreen mode Exit fullscreen mode

In the backend I updated the routes.rb and created a new comments table with a username and a score.

controller 

class CommentsController < ApplicationController
def index
   @comments = Comment.all
   render json: @comments
end
def create 
    @comment = Comment.create(username: params[:username], comment: params[:comment])
    render json: @comment
end
def show
    @comment = Comment.find(params[:comment])
    render json: @comment
end
end

Enter fullscreen mode Exit fullscreen mode

To get all of these pieces working together I wrapped my index.js with a

index.js
window.addEventListener('DOMContentLoaded', e => { 

}
Enter fullscreen mode Exit fullscreen mode

and my index.html file with an onload function to make my fetch calls

index.html
<body onload="loadpage()">
    <script>
        function loadpage() {
            callUsers(), callComments(), callGames()
        }
    </script>
Enter fullscreen mode Exit fullscreen mode

with async defined in my index.html I could use all of this to leverage asynchronous functions for a smoother user game play(or impatient children)

index.html
  <script src="src/index.js" async></script>
    <script src="src/api.js" async></script>
Enter fullscreen mode Exit fullscreen mode

One of the most important things I learned while working through this project was the attention I needed to have with spelling errors and exactly what I was passing as data.
things like calling user_params vs. params when creating a user...insert eyeroll... or calling HTTP vs HTTPS in get/post requests...insert bigger eyeroll...
Overall I really enjoyed building out this project and learning how to make many things come together with Javascript, an API and css.

Top comments (0)