Let's Build: With Ruby on Rails - Dribbble Clone
Tags: rails, ruby, writing, tutorial
Published: 2019-01-31 16:49:21

Welcome to a five-part Let's Build: With Ruby on Rails series where I teach you how to build a Dribbble clone in Ruby on Rails. This series is our most thorough build yet!

[Dribbble](https://dribble.com) is an active design community where designers of all varieties post their "shots" of whatever it is they are working on. What was originally intended to become more of "show your progress" type of site has become more of a portfolio for up and coming designers as well as seasoned pros. (I happen to work here now 💃)

Our clone will introduce some new concepts as well as others I covered in previous builds. If you landed here and are brand new to Ruby on Rails, I invite you to check out my other series below for some foundational principles behind the framework.

- [An Introduction to Ruby on Rails](https://web-crunch.com/lets-build-with-ruby-on-rails-introduction/)
- [Installing Ruby on Rails on your machine](https://web-crunch.com/lets-build-with-ruby-on-rails-installation/)
- [Build a basic blog with comments using Ruby on Rails](https://web-crunch.com/lets-build-with-ruby-on-rails-blog-with-comments/)
- [Build a Twitter clone with Ruby on Rails](https://web-crunch.com/lets-build-with-ruby-on-rails-a-twitter-clone/)

### What we will be building in depth

Our app will have the following features:

- The ability to create, edit, and destroy "shots" as well as like and unlike individual shots.
- User roles and authentication
- Drag and drop functionality
- Commenting functionality
- View counts/analytics
- A custom responsive shot grid UI using CSS Grid

There's a lot under the hood of this build so I went ahead and created a public repo on Github for you to follow along/reference. For time sake you'll notice in some of the following videos that I copied and pasted some general styles and mark up

#### Download the Source Code

{% github https://github.com/justalever/dribbble_clone %}

### What won't be featured in the build

- Independent user profiles
- Search
- Tagging
- Custom color palette generation as Dribbble currently does.
- A ton of other cool features

There's a ton we could do to extend the clone but since it's an "example" type of application I decided to forgo extending it dramatically for now. In the future, I may add to this series and extend it further but I mostly invite you to do that same on your own to see how you could mimic the real app even more.

### Watch Part 1

{% youtube u3-tZMKYegg %}


### Watch Part 2

{% youtube 7rti-QamKlg %}

### Watch Part 3

{% youtube ywMt-R5QIHU %}

### Watch Part 4

{% youtube vP0lMyO59Yc %}

### Watch Part 5

{% youtube LSMbwIUS558 %}


### Getting started

Assuming you have rails and ruby installed on your machine ([learn how to here](https://web-crunch.com/lets-build-with-ruby-on-rails-installation/)) you are ready to begin. From your favorite working directory using your favorite command line tool run the following:

<pre class="language-bash"><code>$ rails new dribbble_clone</code></pre>

This should scaffold a new rails project and create the folder called `dribbble_clone`. 

To kick things off I've added all the gems from this project to my `Gemfile`. You can reference the repo if you haven't already to grab the same versions I've used here.

#### The Gem List

Our gem list has grown since the previous build. You'll find the larger your app becomes the more gems you'll need. 

- [CarrierWave](https://github.com/carrierwaveuploader/carrierwave) + [Mini Magick](https://github.com/minimagick/minimagick) - For image uploads and optimization
- [Devise](https://github.com/plataformatec/devise) - User authentication and roles
- [Guard](https://github.com/guard/guard) - Doing work to files as they change - A task runner of sorts
- [Guard Livereload](https://github.com/guard/guard-livereload) - Reloads the browser when files change combined with the Live Reload extension on your favorite browser.
- [Better Errors](https://github.com/charliesome/better_errors) - Displays better errors during development. 
- [Simple Form](https://github.com/plataformatec/simple_form) - Simpler forms in Rails
- [Bulma Rails](https://github.com/joshuajansen/bulma-rails) - My favorite CSS framework as of late based on Flexbox.
- [Impressionist](https://github.com/charlotte-ruby/impressionist) - We use this to get view counts on shots
- [Gravatar Image Tag](https://github.com/mdeering/gravatar_image_tag) - Easy way to grab a user's gravatar image based on their account email
- [Acts As Votable](https://github.com/ryanto/acts_as_votable) - Like and unlike shots

I'll defer to the videos for a guide in getting our project all set up with each individual gem. The bulk of Part 1 is devoted to this process. We customize Devise a bit to add a "name" field for new users who register. Again reference the repo to see what that entails.

## Scaffolding the Shot

We could go through and create a controller, model, and many other files for our Shot but it would much easier and probably more effective to `scaffold` our Shot and remove any files we don't need afterward. Rails lends us a hand by generating all the necessary files and migration files we need when running a scaffold command.

Run the following to generate our `Shot`  with title and description attributes:

<pre class="language-bash"><code>
rails g scaffold Shot title:string description:text user_id:integer
rails db:migrate
</code></pre>

### Associate Users to Shots

Navigate to your `models` direction within the `app` directory. We need to associate users with shots. Amend your files to look like the following. Notice we have some data here already added when we installed Devise. You may see some of these associations already in place. If not they should look like the following at this point:

<pre class="language-ruby"><code>
# app/models/user.rb
class User &lt; ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
  has_many :shots
end
</code></pre>

<pre class="language-ruby"><code>
# app/models/shot.rb
class Shot &lt; ApplicationRecord
	belongs_to :user
end
</code></pre>



### Carrierwave Setup

A foundational piece of a shot is, of course, its image. Carrierwave is a gem for image uploads with a bunch of bells and whistles. Make sure you have installed the [CarrierWave gem](https://github.com/carrierwaveuploader/carrierwave) and then run the following to generate a new uploader file called `user_shot_uploader.rb`. Be sure you also have the [MiniMagick gem](https://github.com/minimagick/minimagick) I referenced installed at this point.

<pre class="language-bash"><code>$ rails generate uploader user_shot</code></pre>


this should give you a file in:

`app/uploaders/user_shot_uploader.rb`

Inside that file, we can customize some bits about how CarrierWave treats our images. My file looks like this when it's all said and done. Note: I've removed some comments that weren't necessary.

<pre class="language-ruby"><code>
class UserShotUploader &lt; CarrierWave::Uploader::Base

  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick

  # Choose what kind of storage to use for this uploader:
  storage :file

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Create different versions of your uploaded files:
  version :full do
    process resize_to_fit: [800, 600]
  end
  version :thumb do
    process resize_to_fit: [400, 300]
  end

  def extension_whitelist
    %w(jpg jpeg gif png)
  end
end
</code></pre>

### Associating our User Shot with a Shot

We need our new user shot image to be a field on our database table. To do this we need to add that as a migration which gives us a `user_shot` column of the type `string`.

<pre class="language-bash"><code>
$ rails g migration add_shot_to_users shot:string
$ rails db:migrate
</code></pre>

Inside the `shot.rb` file within `app/models/shot.rb` add the following code to make it all work.

<pre class="language-ruby"><code>
class Shot &lt; ActiveRecord::Base
  belongs_to :user
  mount_uploader :user_shot, UserShotUploader # add this line
end
</code></pre>

To really allow a user to upload a shot we need to configure our controller to allow all of these new parameters. 

### Creating a new shot

Our shot controller needs some TLC to function properly at this point. Aside from the controller I wanted to introduce some drag and drop type of functionality within the `new` and `edit` views. Dribbble does an automatic image upload as part of their initial Shot creation. To save time I mimiced this but it definitely falls short of how it all works normally. 

The `shots_controller.rb` found in `app/controllers/` looks like this in the end:

<pre class="language-ruby"><code>
class ShotsController &lt; ApplicationController
  before_action :set_shot, only: [:show, :edit, :update, :destroy, :like, :unlike]
  before_action :authenticate_user!, only: [:edit, :update, :destroy, :like, :unlike]
  impressionist actions: [:show], unique: [:impressionable_type, :impressionable_id, :session_hash]

  # GET /shots
  # GET /shots.json
  def index
    @shots = Shot.all.order('created_at DESC')
  end

  # GET /shots/1
  # GET /shots/1.json
  def show
  end

  # GET /shots/new
  def new
    @shot = current_user.shots.build
  end

  # GET /shots/1/edit
  def edit
  end

  # POST /shots
  # POST /shots.json
  def create
    @shot = current_user.shots.build(shot_params)

    respond_to do |format|
      if @shot.save
        format.html { redirect_to @shot, notice: 'Shot was successfully created.' }
        format.json { render :show, status: :created, location: @shot }
      else
        format.html { render :new }
        format.json { render json: @shot.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /shots/1
  # PATCH/PUT /shots/1.json
  def update
    respond_to do |format|
      if @shot.update(shot_params)
        format.html { redirect_to @shot, notice: 'Shot was successfully updated.' }
        format.json { render :show, status: :ok, location: @shot }
      else
        format.html { render :edit }
        format.json { render json: @shot.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /shots/1
  # DELETE /shots/1.json
  def destroy
    @shot.destroy
    respond_to do |format|
      format.html { redirect_to shots_url, notice: 'Shot was successfully destroyed.' }
      format.json { head :no_content }
    end
  end

  def like
    @shot.liked_by current_user
    respond_to do |format|
      format.html { redirect_back fallback_location: root_path }
      format.json { render layout:false }
    end
  end

  def unlike
    @shot.unliked_by current_user
    respond_to do |format|
      format.html { redirect_back fallback_location: root_path }
      format.json { render layout:false }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_shot
      @shot = Shot.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the whitelist through.
    def shot_params
      params.require(:shot).permit(:title, :description, :user_shot)
    end
end
</code></pre>

The `like` and `unlike` actions are specific to our Acts as Voteable gem which I'll discuss soon.  Notice on the `shot_params` private method we are permitting the fields inside `app/views/_form.html.erb`.

### Adding views and drag and drop functionality

The form partial will be reused for both editing and creating new shots. I wanted to utilize some dragging and dropping. To do this I introduced some JavaScript of which does quite a bit of magic to make the process seem fluid. We add classes to a defined drop zone based on where the element is on the browser window. When the image is dropped it gets displayed automatically using a FileReader API built into most modern browsers. It also attaches it's path to a hidden file input of which when a new post is created gets passed through to our create action on the `shots_controller.rb`. 

When it's all said and done our new shot is created and associated with the currently logged in user or `current_user` in this case.

In our form partial inside `app/views/_form.html.erb` I have the following code:

<pre class="language-markup"><code>
&lt;%= simple_form_for @shot, html: { multipart: true }  do |f| %&gt;
  &lt;%= f.error_notification %&gt;
  &lt;div class="columns is-centered"&gt;
    &lt;div class="column is-half"&gt;
    
    &lt;% unless @shot.user_shot.blank? %&gt;
    &lt;%= image_tag (@shot.user_shot_url), id: "previewImage" %&gt;
    &lt;% end %&gt;

    &lt;output id="list"&gt;&lt;/output&gt;
    &lt;div id="drop_zone"&gt;Drag your shot here&lt;/div&gt;
    &lt;br /&gt;

    &lt;%= f.input :user_shot, label: false, input_html: { class: "file-input", type: "file" }, wrapper: false, label_html: { class: "file-label" } %&gt;

    &lt;div class="field"&gt;
      &lt;div class="control"&gt;
      	&lt;%= f.input :title, label: "Title", input_html: { class: "input"}, wrapper: false, label_html: { class: "label" } %&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="field"&gt;
    	&lt;div class="control"&gt;
      	&lt;%= f.input :description, input_html: { class: "textarea"}, wrapper: false, label_html: { class: "label" } %&gt;
      &lt;/div&gt;
    &lt;/div&gt;

    &lt;div class="field"&gt;
    	&lt;div class="control"&gt;
      	&lt;%= f.button :submit, class:"button is-primary" %&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
&lt;% end %&gt;
</code></pre>

Nothing too crazy here. I've added specific markup for Bulma to play nicely with SimpleForm as well as a bit of HTML for our JavaScript to interact with:

<pre class="language-markup"><code>
&lt;output id="list"&gt;&lt;/output&gt;
&lt;div id="drop_zone"&gt;Drag your shot here&lt;/div&gt;
</code></pre>

Our drop zone is where the magic happens. Combined with some CSS we can make changes as a user interacts with the zone. Check the [repo](https://github.com/justalever/dribbble_clone/blob/master/app/assets/stylesheets/application.scss) for the CSS necessary here.

The JavaScript to make all of this work is found in `app/assets/javascripts/shot.js`

<pre class="language-javascript"><code>
document.addEventListener("turbolinks:load", function() {

	var Shots = {
		previewShot() {
			if (window.File &amp;&amp; window.FileList &amp;&amp; window.FileReader) {

				function handleFileSelect(evt) {
					evt.stopPropagation();
					evt.preventDefault();

					let files = evt.target.files || evt.dataTransfer.files; 
					// files is a FileList of File objects. List some properties.
					for (var i = 0, f; f = files[i]; i++) {

						// Only process image files.
						if (!f.type.match('image.*')) {
							continue;
						}
						const reader = new FileReader();

						// Closure to capture the file information.
						reader.onload = (function(theFile) {
							return function(e) {
								// Render thumbnail.
								let span = document.createElement('span');
								span.innerHTML = ['&lt;img class="thumb" src="', e.target.result,
									'" title="', escape(theFile.name), '"/&gt;'
								].join('');
								document.getElementById('list').insertBefore(span, null);
							};
						})(f);

						// Read in the image file as a data URL.
						reader.readAsDataURL(f);
					}
				}

				function handleDragOver(evt) {
					evt.stopPropagation();
					evt.preventDefault();
					evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy.
				}

				// Setup the dnd listeners.
				// https://stackoverflow.com/questions/47515232/how-to-set-file-input-value-when-dropping-file-on-page
				const dropZone = document.getElementById('drop_zone');
				const target = document.documentElement;
				const fileInput = document.getElementById('shot_user_shot');
				const previewImage = document.getElementById('previewImage');
				const newShotForm = document.getElementById('new_shot');


				if (dropZone) {
					dropZone.addEventListener('dragover', handleDragOver, false);
					dropZone.addEventListener('drop', handleFileSelect, false);

					// Drop zone classes itself
					dropZone.addEventListener('dragover', (e) =&gt; {
						dropZone.classList.add('fire');
					}, false);

					dropZone.addEventListener('dragleave', (e) =&gt; {
						dropZone.classList.remove('fire');
					}, false);

					dropZone.addEventListener('drop', (e) =&gt; {
						e.preventDefault();
						dropZone.classList.remove('fire');
						fileInput.files = e.dataTransfer.files;
						// if on shot/id/edit hide preview image on drop
						if (previewImage) {
							previewImage.style.display = 'none';
						}
						// If on shots/new hide dropzone on drop
						if(newShotForm) {
							dropZone.style.display = 'none';
						}
					}, false);

					// Body specific 
					target.addEventListener('dragover', (e) =&gt; {
						e.preventDefault();
						dropZone.classList.add('dragging');
					}, false);

					// removes dragging class to body WHEN NOT dragging
					target.addEventListener('dragleave', (e) =&gt; {
						dropZone.classList.remove('dragging');
						dropZone.classList.remove('fire');
					}, false);
				}
			}
		},
	    // Displays shot title, description, and created_at time stamp on home page on hover.
        // Be sure to include jquery in application.js
		shotHover() {
			$('.shot').hover(function() {
				$(this).children('.shot-data').toggleClass('visible');
			});
		}

	};
	Shots.previewShot();
	Shots.shotHover();

});</code></pre>

I do a full explanation of this code in the videos so be sure to watch to understand what's happening here. 

## Comments Setup

Comments are a big part of the community aspect of Dribbble.  I wanted to add these to the mix as both and exercise as well as an explanation of how to allow users to interact as well as create, edit, and destroy their own comments combined with Devise.

To get started we need to generate a controller.  Here I specify to only create the `create` and `destroy` actions. This also generates those views. You can delete those inside `app/views/comments/`.

<pre class="language-bash"><code>$ rails g controller comments create destroy </code></pre>



To interact with our database we need a Comment model. Run the following: 

<pre class="language-bash"><code>$ rails g model comment name:string response:text</code></pre>

All comments will have a Name and Response field. I don't get into how validation works in this series but to enhance the app I would definitely consider adding these so that a use must enter their name and a worthy response. No empty fields in other words.

After creating the model be sure to run:

<pre class="language-bash"><code>
$ rails db:migrate
</code></pre>

We need to associate a comment to a shot and to do that requires adding a `shot_id` paramter to comments. Rails is smart enough to know that `shot_id` is how a `shot` will reference a comment. This is true for any types of models you associate together.

<pre class="language-bash"><code>
$ rails g migration add_shot_id_to_comments 
</code></pre>

Inside that generation (`db/migrate/XXXXXXXXX_add_shot_id_to_comments`) add the following:

<pre class="language-ruby"><code>
class AddShotIdToComments &lt; ActiveRecord::Migration[5.1]
  def change
  	add_column :comments, :shot_id, :integer
  end
end
</code></pre>

Then run :

<pre class="language-bash"><code>
$ rails db:migrate		
</code></pre>

Next we need to associate the models directly. Inside the comment model add the following:

<pre class="language-ruby"><code>
# app/models/comment.rb
class Comment &lt; ApplicationRecord
	belongs_to :shot
    belongs_to :user
end
</code></pre>

And also update the shot model:

<pre class="language-ruby"><code>
# app/model/shot.rb
class Shot &lt; ApplicationRecord
	belongs_to :user
	has_many :comments, dependent: :destroy # if a shot is deleted so are all of its comments
	mount_uploader :user_shot, UserShotUploader
end
</code></pre>

Update routes to have nested comments within shots:

<pre class="language-ruby"><code>
# config/routes.rb

resources :shots do 
  resources :comments
end
</code></pre>

The logic of creating a comment comes next inside our `comments_controller.rb` file. 

<pre class="language-ruby"><code>
class CommentsController &lt; ApplicationController
  # a user must be logged in to comment
	before_action :authenticate_user!, only: [:create, :destroy]

  def create
    # finds the shot with the associated shot_id
  	@shot = Shot.find(params[:shot_id]) 
     # creates the comment on the shot passing in params 
  	@comment = @shot.comments.create(comment_params)
     # assigns logged in user's ID to
  	@comment.user_id = current_user.id if current_user comment
    # saves it
  	@comment.save!
	# redirect back to Shot
  	redirect_to shot_path(@shot)

  end

  def destroy
  	@shot = Shot.find(params[:shot_id])
  	@comment = @shot.comments.find(params[:id])
  	@comment.destroy
  	redirect_to shot_path(@shot)
  end

  private

  def comment_params 
  	params.require(:comment).permit(:name, :response)
  end
end
</code></pre>

Our views come next. Inside `app/views/comments` we will add two new files that are partials. These ultimately get rendered on our shot show page found in `app/views/shots/shot.html.erb`.

<pre class="language-markup"><code>
&lt;!-- app/views/comments/_form.html.erb --&gt;
&lt;%= simple_form_for([@shot, @shot.comments.build]) do |f| %&gt;

	&lt;div class="field"&gt;
		&lt;div class="control"&gt;
			&lt;%= f.input :name, input_html: { class: 'input'}, wrapper: false, label_html: { class: 'label' } %&gt;
		&lt;/div&gt;
	&lt;/div&gt;

	&lt;div class="field"&gt;
		&lt;div class="control"&gt;
			&lt;%= f.input :response, input_html: { class: 'textarea' }, wrapper: false, label_html: { class: 'label' } %&gt;
		&lt;/div&gt;
	&lt;/div&gt;
	
	&lt;%= f.button :submit, 'Leave a reply', class: 'button is-primary' %&gt;
&lt;% end %&gt;
</code></pre>

This is our comment form of which displays below the shot itself.

<pre class="language-markup"><code>
&lt;!-- app/views/comments/_comment.html.erb --&gt;
&lt;div class="box"&gt;
  &lt;article class="media"&gt;
    &lt;figure class="media-left"&gt;
      &lt;p class="image is-48x48 user-thumb"&gt;
        &lt;%= gravatar_image_tag(comment.user.email.gsub('spam', 'mdeering'), alt: comment.user.name, gravatar: { size: 48 }) %&gt;
      &lt;/p&gt;
    &lt;/figure&gt;
    &lt;div class="media-content"&gt;
      &lt;div class="content"&gt;
        &lt;p&gt;
          &lt;strong&gt;&lt;%= comment.user.name %&gt;&lt;/strong&gt;
          &lt;div&gt;&lt;%= comment.response %&gt;&lt;/div&gt;
        &lt;/p&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;% if (user_signed_in? &amp;&amp; (current_user.id == comment.user_id)) %&gt;
       &lt;%= link_to 'Delete', [comment.shot, comment],
                  method: :delete, class: "button is-danger", data: { confirm: 'Are you sure?' } %&gt;
    &lt;% end %&gt;
  &lt;/article&gt;
&lt;/div&gt;
</code></pre>

This is the actual comment markup itself. Here I decide to grab the logged in users name rather than the name they input on the comment form. If you ever wanted to extend this to allow for publically facing users to comment it's certainly possible. At this point our `comments_controller.rb` requires a user to login or sign up first.

Finally rendering our comment in our `show.html.erb`.

<pre class="language-markup"><code>
&lt;!-- app/views/shots/show.html.erb - Note: This is the final version which includes some other methods from other gems/helpers. Check the repo or videos for the step by step here! --&gt;
&lt;div class="section"&gt;
	&lt;div class="container"&gt;
		&lt;h1 class="title is-3"&gt;&lt;%= @shot.title %&gt;&lt;/h1&gt;
		&lt;div class="columns"&gt;
			&lt;div class="column is-8"&gt;
				&lt;span class="by has-text-grey-light"&gt;by&lt;/span&gt;
				&lt;div class="user-thumb"&gt;
					&lt;%= gravatar_image_tag(@shot.user.email.gsub('spam', 'mdeering'), alt: @shot.user.name, gravatar: { size: 20 }); %&gt;
				&lt;/div&gt;
				&lt;div class="user-name has-text-weight-bold"&gt;&lt;%= @shot.user.name %&gt;&lt;/div&gt;
				&lt;div class="shot-time"&gt;&lt;span class="has-text-grey-light"&gt;posted&lt;/span&gt;&lt;span class="has-text-weight-semibold"&gt;
					&lt;%= verbose_date(@shot.created_at) %&gt;
				&lt;/span&gt;&lt;/div&gt;
			&lt;/div&gt;	
		&lt;/div&gt;

		&lt;div class="columns"&gt;
			&lt;div class="column is-8"&gt;
				&lt;div class="shot-container"&gt;
					&lt;div class="shot-full"&gt;
						 &lt;%= image_tag @shot.user_shot_url unless @shot.user_shot.blank? %&gt;
					&lt;/div&gt;

					&lt;% if user_signed_in? &amp;&amp; (current_user.id == @shot.user_id) %&gt;		
						&lt;div class="buttons has-addons"&gt;
							&lt;%= link_to 'Edit', edit_shot_path(@shot), class: "button" %&gt;
							&lt;%= link_to 'Delete', shot_path, class: "button", method: :delete, data: { confirm: 'Are you sure you want to delete this shot?'} %&gt;
						&lt;/div&gt;
					&lt;% end %&gt;

					&lt;div class="content"&gt;
						&lt;%= @shot.description %&gt;
					&lt;/div&gt;

					&lt;section class="comments"&gt;
						&lt;h2 class="subtitle is-5"&gt;&lt;%= pluralize(@shot.comments.count, 'Comment') %&gt;&lt;/h2&gt;
							&lt;%= render @shot.comments %&gt;
						&lt;hr /&gt;
						&lt;% if user_signed_in? %&gt;
							&lt;div class="comment-form"&gt;
								&lt;h3 class="subtitle is-3"&gt;Leave a reply&lt;/h3&gt;
								&lt;%= render 'comments/form' %&gt;
							&lt;/div&gt;
						&lt;% else %&gt;
							&lt;div class="content"&gt;&lt;%= link_to 'Sign in', new_user_session_path %&gt; to leave a comment.&lt;/div&gt;
						&lt;% end %&gt;
					&lt;/section&gt;

				&lt;/div&gt;
			&lt;/div&gt;
			&lt;div class="column is-3 is-offset-1"&gt;
				&lt;div class="nav panel show-shot-analytics"&gt;
					&lt;div class="panel-block views data"&gt;
						&lt;span class="icon"&gt;&lt;i class="fa fa-eye"&gt;&lt;/i&gt;&lt;/span&gt;
						&lt;%= pluralize(@shot.impressionist_count, 'View') %&gt;
					&lt;/div&gt;
					&lt;div class="panel-block comments data"&gt;
						&lt;span class="icon"&gt;&lt;i class="fa fa-comment"&gt;&lt;/i&gt;&lt;/span&gt;
						&lt;%= pluralize(@shot.comments.count, 'Comment') %&gt;
					&lt;/div&gt;
					&lt;div class="panel-block likes data"&gt;
						&lt;% if user_signed_in? %&gt;
              &lt;% if current_user.liked? @shot %&gt;
                &lt;%= link_to unlike_shot_path(@shot), method: :put, class: "unlike_shot" do %&gt;
                  &lt;span class="icon"&gt;&lt;i class="fa fa-heart has-text-primary"&gt;&lt;/i&gt;&lt;/span&gt;
                  &lt;span class="vote_count"&gt;&lt;%= pluralize(@shot.get_likes.size, 'Like') %&gt;&lt;/span&gt;
                &lt;% end %&gt;
              &lt;% else %&gt;
                &lt;%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %&gt;
                  &lt;span class="icon"&gt;&lt;i class="fa fa-heart"&gt;&lt;/i&gt;&lt;/span&gt;
                  &lt;span class="vote_count"&gt;&lt;%= pluralize(@shot.get_likes.size, 'Like') %&gt;&lt;/span&gt;
                &lt;% end %&gt;
              &lt;% end %&gt;
            &lt;% else %&gt;
                &lt;%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %&gt;
                  &lt;span class="icon"&gt;&lt;i class="fa fa-heart"&gt;&lt;/i&gt;&lt;/span&gt;
                  &lt;span class="vote_count"&gt;&lt;%= pluralize(@shot.get_likes.size, 'Like') %&gt;&lt;/span&gt;
                &lt;% end %&gt;
            &lt;% end %&gt;
					&lt;/div&gt;
				&lt;/div&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>



## Impressionist Setup

Getting unique views on a shot is a nice feature. To make it easier on ourselves I found a gem called [Impressionist](https://github.com/charlotte-ruby/impressionist). It makes impressions quite easy. 

<pre class="language-bash"><code>
$ rails g impressionist
$ rails db:migrate
</code></pre>

If there's an error in specifying which rails version to use you need to append `[5.1]` to the migration. Use whichever version of rails you are on.

Add the following to the `shots_controller.rb` file:

<pre class="language-ruby"><code>
class ShotsController &lt; ApplicationController
    # only recording when a user views the shot page and is unique
	impressionist actions: [:show], unique: [:impressionable_type, :impressionable_id, :session_hash]
  	
	...  
end
</code></pre>

Add the following to the shot.rb file in `app/models/`:

<pre class="language-ruby"><code>
class Shot &lt; ApplicationRecord
	belongs_to :user
	has_many :comments, dependent: :destroy
	
	mount_uploader :user_shot, UserShotUploader
	is_impressionable # adds support for impressionist on our shot model
end
</code></pre>

Display it in views:

<pre class="language-markup"><code>
&lt;!-- on our shot index page app/views/shots/index.html.erb --&gt;
&lt;%= shot.impressionist_count %&gt;
&lt;!-- on our shot show page app/views/shots/show.html.erb --&gt;
&lt;%= @shot.impressionist_count %&gt;
</code></pre>



## Adding Likes with Acts As Votable 

Another nice feature is being able to like and unlike a shot. Dribbble has this functionality in place though theirs does a bit of AJAX behind the scenes to make everything work without a browser refresh. 

### Installation

Add the following to your Gemfile to install the latest release.

<pre class="language-ruby"><code>
gem 'acts_as_votable', '~&gt; 0.11.1' # check if this version is supported in your project
</code></pre>

And follow that up with a `bundle install`.

### Database Migrations

Acts As Votable uses a votes table to store all voting information. To generate and run the migration use.

<pre class="language-bash"><code>
$ rails generate acts_as_votable:migration
$ rake db:migrate
</code></pre>

We need to add the `acts_as_votable` identifier to the model we want votes one. In our case it's `Shot`.

<pre class="language-ruby"><code>
# app/models/shot.rb
class Shot &lt; ApplicationRecord
	belongs_to :user
	has_many :comments, dependent: :destroy

	mount_uploader :user_shot, UserShotUploader
	is_impressionable
	acts_as_votable
end
</code></pre>

I also want to define the User as the _voter_ so we add `acts_as_voter` to the user model.

<pre class="language-ruby"><code>
# app/models/user.rb
class User &lt; ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable
         
  has_many :shots
  has_many :comments, dependent: :destroy
  acts_as_voter

end
</code></pre>

Do create a clickable like or unlike we need to introduce some routes into our app. Below I've added a `member do` block which essentially adds non-restful routes nested within our `shots` restful routes. Running `rake routes` here will show each route within the app. 

<pre class="language-ruby"><code>
Rails.application.routes.draw do
  resources :shots do 
  	resources :comments
  	 member do
    	put 'like', to: "shots#like"
    	put 'unlike', to: "shots#unlike"
  	end
  end
  devise_for :users, controllers: { registrations: 'registrations' }
  root "shots#index"
end
</code></pre>

The controller needs some work here so I've added both the `:like` and `:unlike` actions to the mix. These are also passed through our `before_actions` which handle finding the shot in mention as well as making sure the user is signed in.

<pre class="language-ruby"><code>
# app/controllers/shots_controller.rb

before_action :set_shot, only: [:show, :edit, :update, :destroy, :like, :unlike] 
# sets up shot for like and unlike now keeping things dry.
before_action :authenticate_user!, only: [:edit, :update, :destroy, :like, :unlike] 
# add routes as authenticated. 
....

def like
  @shot.liked_by current_user
  respond_to do |format|
    format.html { redirect_back fallback_location: root_path }
    format.js { render layout: false }
  end
end

def unlike
  @shot.unliked_by current_user
  respond_to do |format|
    format.html { redirect_back fallback_location: root_path }
    format.js { render layout: false }
  end
end
</code></pre>

In our `index.html.erb`  file  within `app/views/show/`we add the following to the "likes" block:

<pre class="language-markup"><code>
&lt;!-- app/views/shots/index.html.erb --&gt;
&lt;div class="level-item likes data"&gt;
   &lt;div class="votes"&gt;
   &lt;% if user_signed_in? %&gt;
      &lt;% if current_user.liked? shot %&gt;
        &lt;%= link_to unlike_shot_path(shot), method: :put, class: 'unlike_shot' do %&gt;
            &lt;span class="icon"&gt;&lt;i class="fa fa-heart has-text-primary"&gt;&lt;/i&gt;&lt;/span&gt;
            &lt;span class="vote_count"&gt;&lt;%= shot.get_likes.size %&gt;&lt;/span&gt; 
        &lt;% end %&gt;
      &lt;% else %&gt;
        &lt;%= link_to like_shot_path(shot), method: :put, class: 'like_shot' do %&gt;
          &lt;span class="icon"&gt;&lt;i class="fa fa-heart"&gt;&lt;/i&gt;&lt;/span&gt;
          &lt;span class="vote_count"&gt;&lt;%= shot.get_likes.size %&gt;&lt;/span&gt; 
        &lt;% end %&gt;
      &lt;% end %&gt;
    &lt;% else %&gt;
        &lt;%= link_to like_shot_path(shot), method: :put, class: 'like_shot' do %&gt;
          &lt;span class="icon"&gt;&lt;i class="fa fa-heart"&gt;&lt;/i&gt;&lt;/span&gt;
          &lt;span class="vote_count"&gt;&lt;%= shot.get_likes.size %&gt;&lt;/span&gt; 
        &lt;% end %&gt;
    &lt;% end %&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>

And then to repeat the functionality in the `show.html.erb` file we add the following:

<pre class="language-markup"><code>
&lt;!-- app/views/shots/show.html.erb --&gt;
&lt;div class="panel-block likes data"&gt;
	&lt;% if user_signed_in? %&gt;
    &lt;% if current_user.liked? @shot %&gt;
      &lt;%= link_to unlike_shot_path(@shot), method: :put, class: "unlike_shot" do %&gt;
        &lt;span class="icon"&gt;&lt;i class="fa fa-heart has-text-primary"&gt;&lt;/i&gt;&lt;/span&gt;
        &lt;span class="vote_count"&gt;&lt;%= pluralize(@shot.get_likes.size, 'Like') %&gt;&lt;/span&gt;
      &lt;% end %&gt;
    &lt;% else %&gt;
      &lt;%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %&gt;
        &lt;span class="icon"&gt;&lt;i class="fa fa-heart"&gt;&lt;/i&gt;&lt;/span&gt;
        &lt;span class="vote_count"&gt;&lt;%= pluralize(@shot.get_likes.size, 'Like') %&gt;&lt;/span&gt;
      &lt;% end %&gt;
    &lt;% end %&gt;
  &lt;% else %&gt;
      &lt;%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %&gt;
        &lt;span class="icon"&gt;&lt;i class="fa fa-heart"&gt;&lt;/i&gt;&lt;/span&gt;
        &lt;span class="vote_count"&gt;&lt;%= pluralize(@shot.get_likes.size, 'Like') %&gt;&lt;/span&gt;
      &lt;% end %&gt;
  &lt;% end %&gt;
&lt;/div&gt;
</code></pre>



### Final Touches

On the home page we can add a bit of marketing to the mix and display a general hero spot for people that are not logged in. If they are logged in I don't want this to display. The end result looks like this withn our index view:

<pre class="language-markup"><code>
&lt;!-- app/views/shots/index.html.erb --&gt;

&lt;%= render 'hero' %&gt; &lt;!-- render the hero partial--&gt;

&lt;section class="section"&gt;
  &lt;div class="shots"&gt;
    &lt;% @shots.each do |shot| %&gt;
    ...
</code></pre>

Within the partial above I have the following. We are checking if a user is not signed in. If they aren't then we display the hero banner. Easy peasy.

<pre class="language-markup"><code>
&lt;% if !user_signed_in? %&gt;
	&lt;section class="hero is-dark"&gt;
		&lt;div class="hero-body"&gt;
			&lt;div class="container has-text-centered"&gt;
				&lt;h1 class="title is-size-5"&gt;
					What are you working on? &lt;span class="has-text-grey-light"&gt;Dribbble is where designers get inspired and hired.&lt;/span&gt;
				&lt;/h1&gt;
				&lt;div class="content"&gt;
					&lt;%= link_to "Login", new_user_session_path, class: "button is-primary" %&gt;
				&lt;/div&gt;
			&lt;/div&gt;
		&lt;/div&gt;
	&lt;/section&gt;
&lt;% end %&gt;
</code></pre>

#### Hover effects

On the index page, I wanted to mimic the hover effect Dribbble has on their shot thumbnails. To do this required a bit of jQuery. I added the following method to our `shots.js` file I spoke of before. Our final `shots.scss` file is also below for reference.

<pre class="language-javascript"><code>
// app/assets/javascripts/shot.js
...
},
shotHover() {
  $('.shot').hover(function() {
      $(this).children('.shot-data').toggleClass('visible');
    });
  }
};
Shots.previewShot();
Shots.shotHover();
  
});

</code></pre>

<pre class="language-css"><code>
/* app/assets/stylesheets/shots.scss */
.hero.is-dark {
	background-color: #282828 !important;
}

.shot-wrapper {
	box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
	border-radius: 2px;
	padding: 10px;
	background: white;
}

.shots {
	display: grid;
	grid-template-columns: repeat(5, 1fr);
	grid-gap: 1rem;
	@media only screen and (min-width: 1600px) {
		grid-template-columns: repeat(6, 1fr);
	}
	@media only screen and (max-width: 1300px) {
		grid-template-columns: repeat(4, 1fr);
	}
	@media only screen and (max-width: 1100px) { 
		grid-template-columns: repeat(3, 1fr);
	}
	@media only screen and (max-width: 800px) {
		grid-template-columns: 1fr 1fr;
	}
	@media only screen and (max-width: 400px) {
		grid-template-columns: 1fr;
	}

}

.shot {
	position: relative;
	display: block;
	color: #333 !important;

	.shot-data {
		display: none;
		position: absolute;
		top: 0;
		right: 0;
		bottom: 0;
		left: 0;
		padding: 10px;
		background: rgba(white, .9);
		cursor: pointer;
		.shot-title {
			font-weight: bold;
		}
		.shot-description {
			font-size: .9rem;
		}
		.shot-time {
			font-size: .8rem;
			padding-top: .5rem;
		}
	}
}

.user-data {
	padding: 1rem 0 0 0;
}

.user-name {
	display: inline-block;
	position: relative;
	top: -4px;
	padding-left: 5px;
}

.user-thumb {
	display: inline-block;
	img {
		border-radius: 50%;
	}
}

.by,
.shot-time {
	display: inline-block;
	position: relative;
	top: -4px;
}

.shot-analytics {
	text-align: right;
	@media only screen and (max-width: 800px) {
		text-align: right;
		.level-item {
			display: inline-block;
			padding: 0 4px;
		}
		.level-left+.level-right {
			margin: 0;
			padding: 0;
		}
		.level-item:not(:last-child) {
			margin: 0;
		}
	}
}

.shot-analytics,
.panel.show-shot-analytics {
	font-size: .9rem;
	a,
	.icon {
		color: #aaa;
	}
	.icon:hover,
	a:hover {
		color: darken(#aaa, 25%);
	}
}

.panel.show-shot-analytics {
	a { color: #333; }
	.icon {
		padding-right: .5rem;
	}
	.likes .vote_count {
		margin-left: -4px;
	}
}


.shot-container {
	box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
	border-radius: 2px;
	padding: 40px;
	background: white;
	@media only screen and (max-width: 800px) {
		padding: 10px;
	}
	.content,
	.comments {
		margin-top: 1rem;
		padding: 20px;
		@media only screen and (max-width: 800px) {
			padding: 0;
		}
	}
}

.shot-full {
	text-align: center;
}
</code></pre>



### Final Thoughts and Thanks

This build was a big endeavor even though from the outside in it probably looks a little light on features. Dribbble has a lot of moving parts and our clone only scratched the surface. I invite you to carry on adding features and integrations where you see fit. Just diving in and breaking and fixing things is what ultimately allows you to learn and grow as a web developer. 

Thanks for tuning in to this series. If you followed along or have feedback please let me know in the comments or on [Twitter](https://twitter.com/webcrunchblog). If you haven't already, be sure to check out the [rest of the blog](https://web-crunch.com/archive) for more articles on anything design and development. 



#### Watch/read the full Ruby on Rails Let's Build Series:

1. [An Introduction to Ruby on Rails](https://web-crunch.com/lets-build-with-ruby-on-rails-introduction/)
2. [Installing Ruby on Rails on your machine](https://web-crunch.com/lets-build-with-ruby-on-rails-installation/)
3. [Build a basic blog with comments using Ruby on Rails](https://web-crunch.com/lets-build-with-ruby-on-rails-blog-with-comments/)
4. [Build a Twitter clone with Ruby on Rails](https://web-crunch.com/lets-build-with-ruby-on-rails-a-twitter-clone/)



## Shameless plugs 

If you liked this post, I have many more builds on [YouTube](https://youtube.com/c/webcrunch) and my [blog](https://web-crunch.com). I plan to start authoring more here as well. Want more content like this in your inbox? [Subscribe to my newsletter](https://web-crunch.com/subcribe) and get it automatically.

<a href="https://hellorails.io" target="_blank">
![](https://thepracticaldev.s3.amazonaws.com/i/wkb5yyst7pxa1s16kr2u.jpg)
</a>
☝ Want to learn Ruby on Rails from the ground up? Check out my upcoming course called [Hello Rails](https://hellorails.io).