How to Build a Pinterest Clone in Ruby on Rails

Based on Mackenzie Child’s Week 4 of his 12 Web Apps in 12 Weeks Series

Oh hey. Didn’t see you there.

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!


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/

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:

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:

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!

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 

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.

Now to run the migration:

rake db:migrate

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

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

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

Let’s create an index action:

class PinsController < ApplicationController
def index 
end

end

How easy was that?

Now for the Index view!

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

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

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

No routes defined!

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

Rails.application.routes.draw do
resources :pins

end

Now, re-run that rake routes command:

rake routes

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.

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:

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 < ApplicationController
def index 
end
def new
@pin = Pin.new
end
def create
@pin = Pin.new(pin_params)
end
private
def 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

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

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

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

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):

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:

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

Ok, creating a pin:

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
private
def 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…

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…

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

Refresh that index page aaaaaaaand:

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…

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!

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…

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:

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…

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

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:

Now to create a new user!

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:

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:

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:

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?" }

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…

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:

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:

You can check your rails version with this command:

rails -v

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:

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

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:

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:

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:

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.

And add an image…

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"

Save!

Restart the server.

Refresh.

Create a new pin.

Annnnnnnnnnnnnnd…..

YES!

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:

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:

After:

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:

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.

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

Should look like this:

Save. Refresh. Annnnnd:

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…

Hmmm. I’ll try updating the image…

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!

Looking sharp!

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.

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:

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)

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…

Looking good:

Ok, now to test the button!

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.

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.

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.

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:

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:

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.

Bye now.