How to Build a Pinterest Clone in Ruby on Rails

Oh hey. Didn’t see you there.

Image for post

In this tutorial you will get a Ruby on Rails Pinterest Clone up and running on your local server, and will push your work into Github.

A few helpful links if you’re just getting started with Ruby on Rails:

I’m on a Mac, so everything is going to be easier for a Mac user to follow along with.

Keep this in mind: it seems to me that your NUMBER 1 job as a developer is to learn how to learn; read, listen, watch… just figure out how the hell to build something from nothing, using any resources you can get your hands on. So, I’m hoping if you get stuck in this tutorial, you can reinforce yourself with the mantra: “Oh wait, I can just google that sh!t”.

House-keeping almost done. Getting closer to lift-off… I’m going to run through this tutorial as I would do them. It’s not necessarily the way to do it, just how I do it, and — loosely — how Mackenzie Child does it in his tutorial.

Let’s dive in!

Image for post

p.s. just an idea: if you get some value out of this article, and you’d like to support the author, purchase your next Amazon shipment through my affiliate link here. Cheers!

Step 1: What are we building?

We are building a pinterest-like application! It’ll look something like this:

Image for post

Features: Pins (image-uploading, title and description for each pin, users can edit and delete) user log-in and authentication, voting, masonry (so that pins scale to the size of the view-port).

Step 2: Navigating the Command line, Creating the app

You may already know how to move about the command line interface, but stick with me here and let’s make sure this content is accessible to those who aren’t familiar yet.

Jumping in… here is an explanation of the commands you’ll see me use in the command line terminal

cd = "change directory"ls = "list files inside directory"rails new pin_board = "rails, please make me a new application called pin_board"rails s = "rails, please start up the local server!"

I’m going to save my new app inside Documents/Projects, so I first need to navigate there in the command line:

cd Documents

then…

cd Projects

then…

rails new pin_board

then…

ls

look at the new files! yay.

then…

rails s

start up the server! Navigate to http://localhost:3000/

Image for post

Great. A nice little vid of this whole process:

Hold down control and hit “c” to kill the server…

Step 3: First Git Commit

Ok, now let’s make a repository to store our work, and make our first git commit…

FYI my github repo for this project is here. After you make a fresh repo, you’ll see:

Image for post

Now to commit and push for the first time…

Here is the order of my commands:

git init
git status
git add .
git status
git commit -m "first commit"
git remote add origin https://github.com/deallen7/pinterest-clone.git
git push -u origin master

See me in action:

Step 4: Opening the App in Sublime, Installing Gems

If you’re using Sublime, here’s a neat little trick that let’s you open the text editor from the command line.

Otherwise, just get Sublime open and find your app. I’m looking at:

Image for post

First thing we’ll do is add some gems…

gem 'haml', '~> 5.0', '>= 5.0.4'
gem 'bootstrap-sass', '~> 3.3', '>= 3.3.7'
gem 'simple_form', '~> 4.0', '>= 4.0.1'

I imagine we’ll be adding devise at some point as well, and maybe a couple other gems, but we’re living in Mackenzie’s world right now so let’s just follow him along!

Image for post

Add these to your gemfile, save your gemfile, and run an install in your terminal:

bundle install

Step 5: Generate a Pin Model (video)

In the command line:

rails g model Pin title:string description:text 
Image for post

This command creates a model and a migration. You can find both of the files in your Ruby app, and I suggest you go hunt them down.

Image for post
Image for post

Now to run the migration:

rake db:migrate
Image for post

Step 6: Generate a Pin Controller, and Setting up an Index Action and View

Moving forward.

We have a model (think: database) for our Pins, now let’s generate a Pin Controller.

NOTE: You should understand the basic MVC framework of a Ruby on Rails web app. Also applies to other frameworks. M = Model, V = View, C = Controller. Here’s a great article on the subject that I recommend you read.

Ok, the controller:

rails g controller Pins
Image for post

and then let’s go find that controller file we just made…

Image for post

Inside the controller… MC: “let’s go ahead and set up some pages…”

Let’s create an index action:

class PinsController < ApplicationControllerdef index 
end

end

How easy was that?

Now for the Index view!

First, create a file called “index.html.haml” underneath app/views/pins

Image for post

Next, I’m going to give the page a header… Just so we know it’s the Index page:

Image for post

Cool. Now, don’t worry, I’m going to tie this all together, but first I want to introduce you to the routes.rb file:

Step 7: Inside the routes.rb file

Before we make any changes to this routes.rb file, try running this command:

rake routes
Image for post

No routes defined!

Now, let’s make a change to our routes file:

Rails.application.routes.draw do
resources :pins

end
Image for post

Now, re-run that rake routes command:

rake routes
Image for post

Neat! Routes! You might think of routes as links. Together with the controller, the routes.rb file can help you set up links through the controller to your views (think: webpage).

The views may display data that you are calling from your model. Remember the MVC structure. The C helps connect the M to the V.

Image for post

Ok, moving forward.

Step 8: Setting the Homepage

We’ve created an Index action in the Pins controller, and we’ve created an Index view. Now, let’s set the homepage of our app to the Index page!

We’ll do this in the routes.rb file:

Rails.application.routes.draw do
resources :pins

root "pins#index"

end

translation:

root "pins#index" = "rails, please make the homepage the index action inside of the pins controller"

The index action is blank though, WTF is going on?

It’s pretty simple. The index action doesn’t have much to do, it’s just going to show the index view, with no data-fetching tasks being passed on to the Model.

Let’s fire up the rails server and see this in action:

Image for post

Cool. We’re flying here. Moving forward.

Step 9: CRUD, Form Building (video)

CRUD = Create, Read, Update, Destroy

These are actions. You might even remember from our routes.rb experiment that these actions also have routes associated with them.

Imagine clicking a “delete” button, or an “edit” button in the browser. Those buttons are connected to routes that are connected to these CRUD actions in the Pins controller.

So, back inside the pins_controller.rb file…

class PinsController < ApplicationControllerdef index 
end
def new
@pin = Pin.new
end
def create
@pin = Pin.new(pin_params)
end
privatedef pin_params
params.require(:pin).permit(:title, :description)
end

end

Translations (Forgive me for any mistaken interpretations here. I’m learning, too!):

  1. We’ve created an action for creating a new pin. It’s a two step process. Inside that action, we’re telling the Controller how to talk to the Model: “please understand that when I say “@pin” I’m trying to create a new entry in the Pin.rb database!”
  2. When I create a new pin, please consider only the information I’m trying to pass inside this instance variable “@pin” and only permit a title and description. I don’t need any other info from you, browser!

That was a sketchy translation at best. Read up on this here.

Next, we’re going to create a view for the New action…

app/views/pins/new.html.haml

Image for post

Now we’re going to create a way for the user to submit the data we’re interested in... A Title & Description for a Pin.

%h1 New form= render 'form'= link_to "Back", root_path

Notice the second line there? We’re asking the browser to render a “partial” called form. It’s sorta like rendering a webpage inside another webpage.

Let’s create that partial:

app/views/pins/_form.html.haml

Image for post

Cool. Now to build the form…

We still need to tinker with setting up simple_form before we can use it…

MC is referencing this document for his simple_form install…

rails g simple_form:install --bootstrap
Image for post

Cool. Back to the form partial… We’re here in the video if you’d like to follow along…

= simple_form_for @pin, html: { multipart: true } do |f|
- if @pin.errors.any?
#errors
%h2
= pluralize(@pin.errors.count, "error")
prevented this Pin from saving
%ul
- @pin.errors.full_messages.each do |msg|
%li = msg
.form-group
= f.input :title, input_html: { class: 'form-control' }
.form-group
= f.input :description, input_html: { class: 'form-control' }
= f.button :submit, class: "btn btn-primary"

Annnnnnnnnd start that server up and navigate to http://localhost:3000/pins/new

Image for post

Neat-o.

Back inside the Pin Controller, let’s add some logic to our Create action:

def create
@pin = Pin.new(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end

If we can save the new pin, redirect to the pin show page and display the notice “Successfully created new Pin”

Else, render the new page again.

You should note that we don’t yet have a show page. We’ll create one shortly…

But first, let’s make sure this notice can show up in our view!

But first, first, let’s rename our application.html.erb file to application.html.haml…

Then we need to re-write the file in HAML…

Why are we doing this? Ask Mackenzie!

If you are using sublime, you can change the syntax highlighting by doing this (sorry, I can’t actually make this gif any bigger):

Image for post

Updated application.html.haml file:

!!! 5
%html
%head
%title Pin Board
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
- flash.each do |name, msg|
= content_tag :div, msg, class: "alert alert-info"
= yield

Now, let’s try creating a new pin:

Image for post

Oops! You’re page might look like this if you forget to rename the application file from .erb to .haml…

Ok, creating a pin:

Image for post

No show action! (And no show view!)

Step 10: Show Action and View

Ok, so let’s create a show action inside the Pin Controller:

def show
end

And then we’re going to create a private method that will be used for the show, edit, update, destroy actions.

We could write this like so:

def show
@pin = Pin.find(params[:id])
end

But instead, because we’re going to use this middle part for several actions…

@pin = Pin.find(params[:id])

…we’re going to move this piece to a private method, and then create a before_action statement that runs this query before processing the show, edit, update or destroy actions. Get it?

The whole controller file looks like so:

class PinsController < ApplicationController
before_action :find_pin, only: [:show, :edit, :update, :destroy]
def index
end
def show
end
def new
@pin = Pin.new
end
def create
@pin = Pin.new(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end
privatedef pin_params
params.require(:pin).permit(:title, :description)
end
def find_pin
@pin = Pin.find(params[:id])
end
end

Ok, now we need that show page, don’t we? We are here in the video.

Inside the app/views/pins/show.html.haml file:

%h1= @pin.title
%p= @pin.description

Fire up the server…

Image for post

Cool.

Now to add a back link…

%h1= @pin.title
%p= @pin.description
=link_to "Back", root_path

Nice.

Neeeext… we’ll list out all of the pins in the Index page…

Step 11: List All Pins in Index Page (video)

Inside the Index View file…

Image for post
- @pins.each do |pin|
%h2= link_to pin.title, pin

Refresh that index page aaaaaaaand:

Image for post

No method error! That’s because our Index action looks like:

def index
end

Let’s do something about that…

def index 
@pins = Pin.all.order("created_at DESC")
end

Refresh the index page…

Image for post

Success.

now to add the ability to Edit and Destroy a Pin…

Step 12: Edit, Update and Destroy a Pin (video)

Before we jump in and create these new actions, remember that we already have written the first part of each of these methods…

before_action :find_pin, only: [:show, :edit, :update, :destroy]

That find_pin method looks like:

def find_pin
@pin = Pin.find(params[:id])
end

So basically, each of these actions already looks like:

def edit
@pin = Pin.find(params[:id])
end
def update
@pin = Pin.find(params[:id])
end
def destroy
@pin = Pin.find(params[:id])
end

Moving forward…

def update
if @pin.update(pin_params)
redirect_to @pin, notice: "Pin was Successfully updated!"
else
render 'edit'
end
end

Guess what? Yep, you got it! We need to create an edit view…

And we need to create an edit link on the show page…

Inside the edit.html.haml file we just created:

%h1 Edit Pin= render 'form'= link_to "Cancel", pin_path

And inside the show page:

%h1= @pin.title
%p= @pin.description
=link_to "Back", root_path
=link_to "Edit", edit_pin_path

Cool. Try it out!

Image for post

It works. Great. GRAND. WONDERFUL.

Now, the ability to destroy (or delete).

def destroy
@pin.destroy
redirect_to root_path
end

And then in our show page, we need a delete link…

%h1= @pin.title
%p= @pin.description
=link_to "Back", root_path
=link_to "Edit", edit_pin_path
=link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }

Refresh our page…

Image for post

Great! Working…

Now, how about we add a link to add a new pin in our index page?

= link_to "New Pin", new_pin_path
- @pins.each do |pin|
%h2= link_to pin.title, pin

It works. Don’t believe me? Check your index page.

OMG I FORGOT WE NEED TO BE KEEPING OUR GIT REPO UP TO DATE

Opps. We’ve added like 20,012 features and we’ve only initialized our repo.

Let’s go ahead and do a commit now…

git status

oh my, look at that red

git add .

ok…

git status

oh my, look at that green

git commit -m "CRUD action and views"

okkk

git push

cool.

For your viewing pleasure:

Image for post

Next, let’s add USERS!

Step 13: Devise Gem, Adding Users (video)

Find the latest and greatest devise gem version at rubygems.org

gem 'devise', '~> 4.4', '>= 4.4.3'

Add to your gemfile…

So now all of the gems we’ve added are looking like dis:

gem 'devise', '~> 4.4', '>= 4.4.3'
gem 'haml', '~> 5.0', '>= 5.0.4'
gem 'bootstrap-sass', '~> 3.3', '>= 3.3.7'
gem 'simple_form', '~> 4.0', '>= 4.0.1'

Great. Save. And Bundle install!

bundle install

Now, visit the devise docs on github…

Next, let’s do the install!

rails g devise:install

then…

Follow the directions…

Step 1: add this line to the development.rb file under Config/environments…

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

And then we’ve already done step 2…

Step 3, we’ve done. Check the application.html.haml file, lines 9 and 10…

Image for post

Step 4, skip!

Step 5, let’s do it!!!

rails g devise:views 

Ok, moving on.

Let’s generate a devise model.

rails g devise User

and then, we need to run:

rake db:migrate
Image for post

Ok, we’re flying.

Let’s make sure it’s working!

Fire up the server, and navigate to http://localhost:3000/users/sign_up

check it:

Image for post

Now to create a new user!

Image for post

Success.

Now to add associations between our pin.rb model and our user.rb model…

Step 14: Adding Associations (video)

Inside the user.rb file:

class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :pins
end

And inside the pin.rb file:

class Pin < ApplicationRecord
belongs_to :user
end

And now, we need to generate a migration so that our pins have a user_id column!

rails g migration add_user_id_to_pins user_id:integer:index

And then…

rake db:migrate

Great. Check it out:

Image for post

Onward.

Let’s jump into the rails console!

rails c

and then

@pin = Pin.first

and then, we’re going to assign any saved pins to the first user…

@user = User.first

then…

@pin.user = @user

And then…

@pin.save

Cool. All together now:

Image for post

Step 15: Customizing User Views (video)

Now, let’s give each of our pins an author:

Inside the show.html.haml file:

%h1= @pin.title
%p= @pin.description
%p= @pin.user.email
=link_to "Back", root_path
=link_to "Edit", edit_pin_path
=link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }

And then let’s check the view:

Image for post

Cool. Please, no spam.

Maybe we even do something like:

%h1= @pin.title
%p= @pin.description
%p
Submitted by
= @pin.user.email
%br/
=link_to "Back", root_path
=link_to "Edit", edit_pin_path
=link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }
Image for post

Neat-o.

Now we need to make sure that all new pins are associated with a user…

Back inside the controller…

We need to update both our new and create actions…

def new
@pin = current_user.pins.build
end
def create
@pin = current_user.pins.build(pin_params)
if @pin.save
redirect_to @pin, notice: "Successfully created new Pin"
else
render 'new'
end
end

Now, we should be able to create a new pin, and it’ll be associated with the user…

Image for post

Yesssss.

Ok, time for a commit:

git status
git add .
git commit -m "added devise gem and user / pin association"
git push

Cool. Moving onwards. Time to style!

Step 16: Adding Style (video)

We already have the bootstrap sass gem installed, but we still need to make some modifications before we can get going.

If you’re interested, check out the docs here.

First, we need to rename our application.css file to application.css.scss

Next, copy / paste in:

@import "bootstrap-sprockets";
@import "bootstrap";

Save that file.

Go ahead and get the server going and see what bootstrap does right out the gate:

Image for post

At this point, it really matters what version of rails you are using…

I’m using 5.1.6, so I need to follow these directions:

Image for post

You can check your rails version with this command:

rails -v
Image for post

So I need to install jquery and then bundle install before adding the extra bits to application.js…

All of my added gems:

gem 'jquery-rails'
gem 'devise', '~> 4.4', '>= 4.4.3'
gem 'haml', '~> 5.0', '>= 5.0.4'
gem 'bootstrap-sass', '~> 3.3', '>= 3.3.7'
gem 'simple_form', '~> 4.0', '>= 4.0.1'

Run the bundle install…

bundle install

Then copy / paste into the application.js file:

//= require jquery
//= require bootstrap-sprockets

My whole application.js file looks like:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require bootstrap-sprockets
//= require rails-ujs
//= require turbolinks
//= require_tree .

Ok, now to really make things pop…

We’re here in MC’s video…

Inside the application.html.haml file:

!!! 5
%html
%head
%title Pin Board
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%nav.navbar.navbar-default
.container
.navbar-brand= link_to "Pin Board", root_path
- if user_signed_in?
%ul.nav.navbar-nav.navbar-right
%li= link_to "New Pin", new_pin_path
%li= link_to "Account", edit_user_registration_path
%li= link_to "Sign Out", destroy_user_session_path, method: :delete
- else
%ul.nav.navbar-nav.navbar-right
%li= link_to "Sign Up", new_user_registration_path
%li= link_to "Sign In", new_user_session_path
- flash.each do |name, msg|
= content_tag :div, msg, class: "alert alert-info"
= yield

Which gives us a nice navbar for a user that is logged in:

Image for post

And a nice navbar for an unregistered person or un-signed in user:

Image for post

Forward!

Next, let’s get rid of that new pin link in the index.html.haml page…

File should look like:

- @pins.each do |pin|
%h2= link_to pin.title, pin

Then, add a container for the flash messages and the “= yield” to the application.html.haml page:

!!! 5
%html
%head
%title Pin Board
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true
= javascript_include_tag 'application', 'data-turbolinks-track' => true
= csrf_meta_tags
%body
%nav.navbar.navbar-default
.container
.navbar-brand= link_to "Pin Board", root_path
- if user_signed_in?
%ul.nav.navbar-nav.navbar-right
%li= link_to "New Pin", new_pin_path
%li= link_to "Account", edit_user_registration_path
%li= link_to "Sign Out", destroy_user_session_path, method: :delete
- else
%ul.nav.navbar-nav.navbar-right
%li= link_to "Sign Up", new_user_registration_path
%li= link_to "Sign In", new_user_session_path
.container
- flash.each do |name, msg|
= content_tag :div, msg, class: "alert alert-info"
= yield

Makes the home page look like:

Image for post

Now let’s do a little bit of work on the form…

We saved ourselves a little bit of trouble by adding the -bootstrap flag when we first installed simple_form… The form already looks pretty good:

Image for post

But we’re still going to tweak the layout a bit…

Inside the new.html.haml file:

.col-md-6.col-md.offset-3
%h1 New form
= render 'form' = link_to "Back", root_path

HAML is really funky about indentation, so make sure you are tabbing your junk over.

My file looks like:

Image for post

Ok. Onward.

We’ll do the same exact thing to the edit.html.haml page…

.col-md-6.col-md.offset-3
%h1 Edit Pin
= render 'form' = link_to "Cancel", pin_path

Great. Onward.

Git commit time.

git add .
git commit -m "basic structure and styles"
git push

Next, let’s add the ability to upload images…

Step 17: Paperclick Gem, Add Ability to Upload Images (video)

Add to the gemfile:

gem 'paperclip', '~> 6.0'

And then bundle install. You know how that cookie is crumbled by now…

Now, on to the github docs for paperclip…

Ugh, it’s deprecated

Let’s see if we can’t use it anyways :)

Inside the pin.rb model file:

class Pin < ApplicationRecord
belongs_to :user
has_attached_file :image, styles: { medium: "300x300>" }
validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/
end

and then let’s run a migration:

rails g paperclip pin image

Next…

Don’t forget to run a migraiton!

rake db:migrate

Great. Now to add a new form option to our form partial…

Whole file looks like:

= simple_form_for @pin, html: { multipart: true } do |f|
- if @pin.errors.any?
#errors
%h2
= pluralize(@pin.errors.count, "error")
prevented this Pin from saving
%ul
- @pin.errors.full_messages.each do |msg|
%li = msg
.form-group
= f.input :image, input_html: { class: 'form-control' }
.form-group
= f.input :title, input_html: { class: 'form-control' }
.form-group
= f.input :description, input_html: { class: 'form-control' }
= f.button :submit, class: "btn btn-primary"

And then we need to add an additional allowed parameter to the pins params…

Inside the controller… add :image…

def pin_params
params.require(:pin).permit(:title, :description, :image)
end

And then inside the show.html.haml file… That first line of code is fresh, new, ya know…

= image_tag @pin.image.url(:medium)
%h1= @pin.title
%p= @pin.description
%p
Submitted by
= @pin.user.email
%br/
=link_to "Back", root_path
=link_to "Edit", edit_pin_path
=link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }

Ok, let’s see this in action!

Spin up the server and refresh the page! Try adding a new pin with an image.

Image for post

And add an image…

Image for post

Curse you!!!!!!!

Ok, let’s try googling “Could not run the `identify` command. Please install ImageMagick.”

I found this resource. Let’s try it out.

First, running:

brew update

And then running:

brew install imagemagick

Okkkkk. Inside the development.rb file:

# Paperclip config:
Paperclip.options[:image_magick_path] = "/opt/ImageMagick/bin"
Paperclip.options[:command_path] = "/opt/ImageMagick/bin"
Image for post

Save!

Restart the server.

Refresh.

Create a new pin.

Annnnnnnnnnnnnnd…..

YES!

Image for post

Ok, moving forward.

Inside the index.html.haml file:

- @pins.each do |pin|
= link_to (image_tag pin.image.url(:medium)), pin
%h2= link_to pin.title, pin

Save, and let’s check out the index file…

Ok, I need to add some images to the first 2 posts…

Ok, great! Look good:

Image for post

Now, for a change to the edit.html.haml form so that if there is already an image uploaded, and we want to change the image, we’ll be able to see the uploaded image…

Code:

.col-md-6.col-md.offset-3
%h1 Edit Pin
= image_tag @pin.image.url(:medium)
= render 'form'
= link_to "Cancel", pin_path

Before:

Image for post

After:

Image for post

Cool. Forward!

Git commit!

git add .
git commit -m "add image uploading with paperclip"
git push

Cool.

Now for Masonry and jQuery!

Step 18: Masonry (video)

Find the latest gemfile at rubygems.org…

gem 'masonry-rails', '~> 0.2.4'

Add to your gemfile…

All of the gems I’ve added:

gem 'masonry-rails', '~> 0.2.4'
gem 'paperclip', '~> 6.0'
gem 'jquery-rails'
gem 'devise', '~> 4.4', '>= 4.4.3'
gem 'haml', '~> 5.0', '>= 5.0.4'
gem 'bootstrap-sass', '~> 3.3', '>= 3.3.7'
gem 'simple_form', '~> 4.0', '>= 4.0.1'

Neat. Now check out the documentation on github…

Inside application.js:

//= require masonry/jquery.masonry

My whole file looks like:

// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require jquery
//= require masonry/jquery.masonry
//= require bootstrap-sprockets
//= require rails-ujs
//= require turbolinks
//= require_tree .

Now, we need to add some junky stuff to our pins.coffee file:

$ ->
$('#pins').imagesLoaded ->
$('#pins').masonry
itemSelector: '.box'
isFitWidth: true

Ok, and now let’s start styling a bit.

Inside the index.html.haml file:

#pins.transitions-enabled
- @pins.each do |pin|
.box.panel.panel-default
= link_to (image_tag pin.image.url(:medium)), pin
.panel-body
%h2= link_to pin.title, pin

Cool. Let’s make sure it works in the browser:

Image for post

Yep. Looks like MC’s. Onward.

Now, to copy / paste some styles from MC. His stylesheet is here.

My stylesheet:

/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require 'masonry/transitions'
*= require_tree .
*= require_self
*/
@import "bootstrap-sprockets";
@import "bootstrap";
body {
background: #E9E9E9;
}
h1, h2, h3, h4, h5, h6 {
font-weight: 100;
}
nav {
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.22);
.navbar-brand {
a {
color: #BD1E23;
font-weight: bold;
&:hover {
text-decoration: none;
}
}
}
}
#pins {
margin: 0 auto;
width: 100%;
.box {
margin: 10px;
width: 350px;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.22);
border-radius: 7px;
text-align: center;
img {
max-width: 100%;
height: auto;
}
h2 {
font-size: 22px;
margin: 0;
padding: 25px 10px;
a {
color: #474747;
}
}
.user {
font-size: 12px;
border-top: 1px solid #EAEAEA;
padding: 15px;
margin: 0;
}
}
}
#edit_page {
.current_image {
img {
display: block;
margin: 20px 0;
}
}
}
#pin_show {
.panel-heading {
padding: 0;
}
.pin_image {
img {
max-width: 100%;
width: 100%;
display: block;
margin: 0 auto;
}
}
.panel-body {
padding: 35px;
h1 {
margin: 0 0 10px 0;
}
.description {
color: #868686;
line-height: 1.75;
margin: 0;
}
}
.panel-footer {
padding: 20px 35px;
p {
margin: 0;
}
.user {
padding-top: 8px;
}
}
}
textarea {
min-height: 250px;
}

Ok. Forward. Looking a little better but the images are looking a little funky.

Image for post

MC to the rescue! It’s because we didn’t tab our index.html.haml file properly.

Should look like this:

Image for post

Save. Refresh. Annnnnd:

Image for post

Bingo.

Hmmm. The images aren’t full width. MC has the answer!

Just update the 4th line in the index.html.haml file to:

= link_to (image_tag pin.image.url), pin

There. At least 2 of the images are working… Not sure why Dwight is struggling…

Image for post

Hmmm. I’ll try updating the image…

Image for post

Ok. Working. Onwards and forwards!

Now to style the show.html.haml page:

#pin_show.row
.col-md-8.col-md-offset-2
.panel.panel-default
.panel-heading.pin_image
= image_tag @pin.image.url(:medium)
.panel-body
%h1= @pin.title
%p.description= @pin.description
.panel-footer
.row
.col-md-6
%p.user
Submitted by
= @pin.user.email
.col-md-6
.btn-group.pull-right
=link_to "Edit", edit_pin_path, class: "btn btn-default"
=link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-default"

Make sure to be detailed about your indentations!

Image for post

Looking sharp!

Image for post

Let’s also add a user under the title on the index page:

#pins.transitions-enabled
- @pins.each do |pin|
.box.panel.panel-default
= link_to (image_tag pin.image.url), pin
.panel-body
%h2= link_to pin.title, pin
%p.user
Submitted by
= pin.user.email

Cool! Sharp. Ouchy.

Image for post

Moving on…

Git time.

git add .
git commit -m "add masonry and styles"
git push

Next, for the voting feature!

Step 19: Acts as Votable (video)

You know the drill. Rubygems.org for the latest and greatest.

gem 'acts_as_votable', '~> 0.11.1'

Save in the gemfile and run the bundle install…

And then check out the documentation

Docs say we need to run a migration. Of course we do! We need a place to save the votes.

rails generate acts_as_votable:migration

then run the migration through…

rake db:migrate

Looks like:

Image for post

Big ol table. Neat.

Now inside the pin.rb file:

class Pin < ApplicationRecord
acts_as_votable
belongs_to :user
has_attached_file :image, styles: { medium: "300x300>" }
validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/
end

Save.

Now for some routes!

Inside the routes.rb file…

Rails.application.routes.draw do
devise_for :users
resources :pins do
member do
put "like", to: "pins#upvote"
end
end

root "pins#index"

end

Cool. New paths! Notice we aren’t doing any down-voting (we could, we just aren’t)

Image for post

Next, inside the pin controller…

We’re going to create a new action called “upvote”

And we’re going to add the action to the before_action…

before_action :find_pin, only: [:show, :edit, :update, :destroy, :upvote]

And inside the upvote action:

def upvote
@pin.upvote_by current_user
redirect_to :back
end

Then, inside the show page…

We need a link to vote!

#pin_show.row
.col-md-8.col-md-offset-2
.panel.panel-default
.panel-heading.pin_image
= image_tag @pin.image.url(:medium)
.panel-body
%h1= @pin.title
%p.description= @pin.description
.panel-footer
.row
.col-md-6
%p.user
Submitted by
= @pin.user.email
.col-md-6
.btn-group.pull-right
=link_to like_pin_path(@pin), method: :put, class: "btn btn-default" do
%span.glyphicon.glyphicon-heart
= @pin.get_upvotes.size
=link_to "Edit", edit_pin_path, class: "btn btn-default"
=link_to "Delete", pin_path, method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-default"

Again, be careful about those indentations or you might have some errors…

Image for post

Looking good:

Image for post

Ok, now to test the button!

Image for post

Ew. Don’t be scared though. Someone has already made this mistake. Let’s google “undefined method ‘back_url”

Sure enough, here’s the answer.

Image for post

I’ll update my upvote method to:

def upvote
@pin.upvote_by current_user
redirect_back fallback_location: root_path
end

SAve. And refresh…

Yay.

Image for post

Ok. Onwards.

Git time.

git add .
git commit -m "add and configure acts as votable"
git push

Neato.

Step 20: Authentication

Oh bollocks. Time to work backwards for a minute.

Need to add…

- if user_signed_in?

…above the edit and delete buttons to prevent those buttons from showing up to anyone but the user who made the pin.

Image for post

Next, we need to add an authenticated before action to the pins controller…

before_action :authenticate_user!, except: [:index, :show]

Great. Now non-users can only view pins, but they can’t CUD (create, update, or delete).

Ok. More styling…

Step 21: More Style (video)

Inside the new.html.haml file:

.col-md-6.col-md.offset-3
.row
.panel.panel-default
.panel-heading
%h1 New form
.panel-body
= render 'form'

Cool:

Image for post

Ok, inside views/devise/registrations/edit.html.erb…

Just copy / paste this mess from MC’s repo

<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Edit Your Account</h2>
</div>
<div class="panel-body">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
<div>Currently waiting confirmation for: <%= resource.unconfirmed_email %></div>
<% end %>
<div class="form-group">
<%= f.label :password %> <i>(leave blank if you don't want to change it)</i><br />
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :current_password %> <i>(we need your current password to confirm your changes)</i><br />
<%= f.password_field :current_password, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.submit "Update", class: "btn btn-primary" %>
</div>
<% end %>
</div>
<div class="panel-footer">
<h3>Cancel my account</h3>
<p>Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete, class: "btn btn-default" %></p>
<br>
<%= link_to "Back", :back, class: "btn btn-default" %>
</div>
</div>
</div>
</div>

And then inside the new.html.erb file:

<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Sign up</h2>
</div>
<div class="panel-body">
<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
<%= devise_error_messages! %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password %> <% if @validatable %><i>(<%= @minimum_password_length %> characters minimum)</i><% end %><br />
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %>
</div>
<div class="form-group">
<%= f.submit "Sign up", class: "btn btn-primary" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
</div>
</div>
</div>
</div>

And now let’s tackle the pin edit.html.haml page:

#edit_page.col-md-8.col-md-offset-2
.row
.panel.panel-default
.panel-heading
%h1 Edit Your Pin
.panel-body
.current_image
%strong.center Current Image
= image_tag @pin.image.url(:medium)
= render 'form'

Ok. Looking good:

Image for post

Ok, now to update the devise/sessions/new.html.erb page…

<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="panel panel-default">
<div class="panel-heading">
<h2>Sign In</h2>
</div>
<div class="panel-body">
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
<div class="form-group">
<%= f.label :email %><br />
<%= f.email_field :email, autofocus: true, class: "form-control" %>
</div>
<div class="form-group">
<%= f.label :password %><br />
<%= f.password_field :password, autocomplete: "off", class: "form-control" %>
</div>
<% if devise_mapping.rememberable? -%>
<div class="form-group">
<%= f.check_box :remember_me %> <%= f.label :remember_me %>
</div>
<% end -%>
<div class="form-group">
<%= f.submit "Log in", class: "btn btn-primary" %>
</div>
<% end %>
<%= render "devise/shared/links" %>
</div>
</div>
</div>
</div>

Okkkk…. Are we done yet MC?

I think we’re done. We’re done. I’m done.

Git time!

git add .
git commit -m "finish updating style"
git push

Wow. Damn. Done.

That was a SPRINT.

Hope you learned something! Thanks for following along. My github is here in case you’re looking for it.

Image for post

Bye now.

Thanks for reading! I hope this article was helpful.

If you want to support the author, the best way is to purchase your next Amazon shipment through my affiliate link here. Or, consider buying me a coffee through venmo (David-Allen-13), cash app (deallen7), or paypal (davideallen).

Your donations, comments, and claps keep me motivated to create more material. I appreciate you! :D

Written by

Documentation and tutorials on Python, Programming, and Data Analysis. FPL Addict. Occasionally writing about biohacking, PMing, and food.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store