Давайте создадим клон Dribbble с помощью Ruby on Rails
Первоначально опубликовано на веб-сайте web-crunch.com 18 декабря 2017 г.
Добро пожаловать в мини-сериал из пяти частей, в котором я научу вас создавать клон Dribbble в Ruby on Rails. Эта серия — наша самая тщательная сборка!
Dribbble — это активное дизайнерское сообщество, где дизайнеры всех мастей публикуют свои снимки того, над чем они работают. То, что изначально задумывалось как сайт типа показать свой прогресс, стало скорее портфолио для начинающих дизайнеров, а также опытных профессионалов.
Наш клон Dribbble представит некоторые новые концепции, а также другие, о которых я рассказывал в предыдущих сборках. Если вы попали сюда и совершенно не знакомы с Ruby on Rails, я приглашаю вас ознакомиться с другими моими сериями ниже, чтобы узнать о некоторых основополагающих принципах, лежащих в основе фреймворка.
- Введение в Ruby on Rails
- Установка Ruby on Rails на вашу машину
- Создайте простой блог с комментариями, используя Ruby on Rails
- Создайте клон Twitter с помощью Ruby on Rails
Что мы будем строить в деталях
Наше приложение будет иметь следующие функции:
- Возможность создавать, редактировать и уничтожать «кадры», а также ставить лайки и отличать отдельные кадры.
- Роли пользователей и аутентификация
- Функция перетаскивания
- Функция комментирования
- Просмотр счетчиков/аналитики
- Пользовательский адаптивный пользовательский интерфейс сетки снимков с использованием CSS Grid.
Под капотом этой сборки много всего, поэтому я пошел дальше и создал общедоступный репозиторий на Github, чтобы вы могли следить за ним/справкой. Ради экономии времени вы заметите, что в некоторых из следующих видео я скопировал и вставил некоторые общие стили и разметку.
Чего не будет в сборке
- Независимые профили пользователей
- Поиск
- Маркировка
- Генерация пользовательской цветовой палитры, как в настоящее время делает Dribbble.
Мы могли бы многое сделать для расширения клона, но, поскольку это приложение типа «пример», я решил пока воздержаться от его радикального расширения. В будущем я могу дополнить эту серию и расширить ее, но в основном я предлагаю вам сделать то же самое самостоятельно, чтобы увидеть, как вы можете еще больше имитировать настоящее приложение.
Смотреть часть 1
Смотреть часть 2
Смотреть часть 3
Смотреть часть 4
Смотреть часть 5
Предполагая, что на вашем компьютере установлены rails и ruby (узнайте, как это сделать), вы готовы начать. Из вашего любимого рабочего каталога с помощью вашего любимого инструмента командной строки выполните следующее:
$ rails new dribbble_clone
Это должно создать новый проект rails и создать папку с именем dribbble_clone
.
Для начала я добавил все драгоценные камни из этого проекта в свой файл Gemfile
. Вы можете сослаться на репо, если вы еще не взяли те же версии, которые я использовал здесь.
Список драгоценных камней
Наш список драгоценных камней расширился по сравнению с предыдущей сборкой. Вы обнаружите, что чем больше становится ваше приложение, тем больше драгоценных камней вам понадобится.
- CarrierWave + Mini Magick — Для загрузки изображений и оптимизации
- Devise — Аутентификация пользователей и роли
- Guard — работа с файлами по мере их изменения — своего рода средство запуска задач
- Guard Livereload — перезагружает браузер при изменении файлов в сочетании с расширением Live Reload в вашем любимом браузере.
- Лучшие ошибки — отображает лучшие ошибки во время разработки.
- Simple Form — Простые формы в Rails
- Bulma Rails — мой любимый CSS-фреймворк в последнее время, основанный на Flexbox.
- Импрессионист — мы используем это для подсчета просмотров кадров.
- Тег Gravatar Image — простой способ получить изображение пользователя для Gravatar на основе электронной почты его учетной записи.
- Действует как право голоса — похожие и непохожие снимки
Я полагаюсь на видео в качестве руководства по настройке нашего проекта с каждым отдельным драгоценным камнем. Этому процессу посвящена основная часть первой части. Мы немного настраиваем Devise, чтобы добавить поле «имя» для новых пользователей, которые регистрируются. Снова обратитесь к репо, чтобы увидеть, что это влечет за собой.
Подмости для выстрела
Мы могли бы пройти и создать контроллер, модель и многие другие файлы для нашего Shot, но было бы намного проще и, вероятно, эффективнее scaffold
наш Shot и впоследствии удалить все файлы, которые нам не нужны. Rails протягивает нам руку, создавая все необходимые файлы и файлы миграции, которые нам нужны при запуске команды scaffold.
Запустите следующее, чтобы сгенерировать наш Shot
с атрибутами title и description:
rails g scaffold Shot title:string description:text user_id:integer rails db:migrate
Свяжите пользователей с снимками
Перейдите в направлении models
в каталоге app
. Нам нужно связать пользователей с выстрелами. Измените свои файлы, чтобы они выглядели следующим образом. Обратите внимание, что некоторые данные здесь уже добавлены, когда мы установили Devise. Вы можете увидеть, что некоторые из этих ассоциаций уже существуют. Если нет, то на данный момент они должны выглядеть следующим образом:
# app/models/user.rb 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 :shots end
# app/models/shot.rb class Shot < ApplicationRecord belongs_to :user end
Настройка несущей
Основополагающая часть кадра — это, конечно же, его образ. Carrierwave — это жемчужина для загрузки изображений с кучей наворотов. Убедитесь, что вы установили гем CarrierWave, а затем запустите следующее, чтобы создать новый файл загрузчика с именем user_shot_uploader.rb
. Убедитесь, что у вас также установлен MiniMagick gem, на который я ссылался.
$ rails generate uploader user_shot
это должно дать вам файл в:
app/uploaders/user_shot_uploader.rb
Внутри этого файла мы можем настроить некоторые параметры того, как CarrierWave обрабатывает наши изображения. Мой файл выглядит так, когда все сказано и сделано. Примечание: я удалил некоторые ненужные комментарии.
class UserShotUploader < 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
Связывание нашего пользовательского снимка с снимком
Нам нужно, чтобы наше новое изображение, снятое пользователем, было полем в нашей таблице базы данных. Для этого нам нужно добавить это как миграцию, которая дает нам столбец user_shot
типа string
.
$ rails g migration add_shot_to_users shot:string
$ rails db:migrate
Внутри файла shot.rb
внутри app/models/shot.rb
добавьте следующий код, чтобы все заработало.
class Shot < ActiveRecord::Base
belongs_to :user
mount_uploader :user_shot, UserShotUploader # add this line
end
Чтобы действительно позволить пользователю загружать снимок, нам нужно настроить наш контроллер, чтобы разрешить все эти новые параметры.
Создание нового снимка
На этом этапе нашему контроллеру выстрела требуется некоторая TLC для правильной работы. Помимо контроллера, я хотел ввести некоторую функциональность типа перетаскивания в представлениях new
и edit
. Dribbble выполняет автоматическую загрузку изображений как часть их первоначального создания Shot. Чтобы сэкономить время, я сымитировал это, но это определенно не соответствует тому, как все это работает нормально.
shots_controller.rb
, найденный в app/controllers/
, в итоге выглядит так:
class ShotsController < 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
Действия like
и unlike
относятся к нашему драгоценному камню Acts as Voteable, о котором я расскажу позже. Обратите внимание, что в частном методе shot_params
мы разрешаем поля внутри app/views/_form.html.erb
.
Добавление представлений и функций перетаскивания
Частичная форма будет повторно использоваться как для редактирования, так и для создания новых кадров. Я хотел использовать перетаскивание. Чтобы сделать это, я представил немного JavaScript, который творит немного магии, чтобы процесс казался плавным. Мы добавляем классы в определенную зону перетаскивания в зависимости от того, где элемент находится в окне браузера. Когда изображение перетаскивается, оно автоматически отображается с помощью API FileReader, встроенного в большинство современных браузеров. Он также прикрепляет свой путь к скрытому входному файлу, который при создании нового сообщения передается нашему действию создания на странице shots_controller.rb
.
Когда все сказано и сделано, наш новый снимок создается и связывается с текущим зарегистрированным пользователем или current_user
в данном случае.
В нашей частичной форме внутри app/views/_form.html.erb
у меня есть следующий код:
<%= simple_form_for @shot, html: { multipart: true } do |f| %>
<%= f.error_notification %>
<div class="columns is-centered">
<div class="column is-half">
<% unless @shot.user_shot.blank? %>
<%= image_tag (@shot.user_shot_url), id: "previewImage" %>
<% end %>
<output id="list"></output>
<div id="drop_zone">Drag your shot here</div>
<br />
<%= f.input :user_shot, label: false, input_html: { class: "file-input", type: "file" }, wrapper: false, label_html: { class: "file-label" } %>
<div class="field">
<div class="control">
<%= f.input :title, label: "Title", input_html: { class: "input"}, wrapper: false, label_html: { class: "label" } %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.input :description, input_html: { class: "textarea"}, wrapper: false, label_html: { class: "label" } %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.button :submit, class:"button is-primary" %>
</div>
</div>
</div>
</div>
<% end %>
Здесь нет ничего слишком сумасшедшего. Я добавил специальную разметку для Bulma, чтобы она хорошо работала с SimpleForm, а также немного HTML для взаимодействия с нашим JavaScript:
<output id="list"></output>
<div id="drop_zone">Drag your shot here</div>
В нашей зоне высадки происходит волшебство. В сочетании с некоторыми CSS мы можем вносить изменения, когда пользователь взаимодействует с зоной. Проверьте репозиторий на наличие необходимого здесь CSS.
JavaScript для выполнения всей этой работы находится в app/assets/javascripts/shot.js
.
document.addEventListener("turbolinks:load", function() {
var Shots = {
previewShot() {
if (window.File && window.FileList && 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 = ['<img class="thumb" src="', e.target.result,
'" title="', escape(theFile.name), '"/>'
].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) => {
dropZone.classList.add('fire');
}, false);
dropZone.addEventListener('dragleave', (e) => {
dropZone.classList.remove('fire');
}, false);
dropZone.addEventListener('drop', (e) => {
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) => {
e.preventDefault();
dropZone.classList.add('dragging');
}, false);
// removes dragging class to body WHEN NOT dragging
target.addEventListener('dragleave', (e) => {
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();
});
Я делаю полное объяснение этого кода в видео, поэтому обязательно посмотрите, чтобы понять, что здесь происходит.
Настройка комментариев
Комментарии являются важной частью сообщества Dribbble. Я хотел добавить их в смесь как упражнения, так и объяснение того, как позволить пользователям взаимодействовать, а также создавать, редактировать и уничтожать свои собственные комментарии в сочетании с Devise.
Для начала нам нужно сгенерировать контроллер. Здесь я указываю создавать только действия create
и destroy
. Это также генерирует эти представления. Вы можете удалить их внутри app/views/comments/
.
$ rails g controller comments create destroy
Для взаимодействия с нашей базой данных нам нужна модель комментариев. Запустите следующее:
$ rails g model comment name:string response:text
Все комментарии будут иметь поля Имя и Ответ. Я не вникаю в то, как работает валидация в этой серии, но для улучшения приложения я бы определенно подумал о том, чтобы добавить их, чтобы пользователь должен был ввести свое имя и достойный ответ. Другими словами, никаких пустых полей.
После создания модели обязательно запустите:
$ rails db:migrate
Нам нужно связать комментарий с кадром, а для этого необходимо добавить параметр shot_id
к комментариям. Rails достаточно умен, чтобы знать, что shot_id
— это то, как shot
будет ссылаться на комментарий. Это верно для любых типов моделей, которые вы связываете вместе.
$ rails g migration add_shot_id_to_comments
Внутри этого поколения (db/migrate/XXXXXXXXX_add_shot_id_to_comments
) добавьте следующее:
class AddShotIdToComments < ActiveRecord::Migration[5.1]
def change
add_column :comments, :shot_id, :integer
end
end
Затем запустите:
$ rails db:migrate
Далее нам нужно связать модели напрямую. Внутри модели комментария добавьте следующее:
# app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :shot
belongs_to :user
end
А также обновить модель выстрела:
# app/model/shot.rb
class Shot < 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
Обновите маршруты, чтобы в кадрах были вложенные комментарии:
# config/routes.rb
resources :shots do
resources :comments
end
Далее идет логика создания комментария внутри нашего файла comments_controller.rb
.
class CommentsController < 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
Дальше идут наши взгляды. Внутри app/views/comments
мы добавим два новых файла, которые являются частичными. В конечном итоге они отображаются на нашей странице показа снимков в app/views/shots/shot.html.erb
.
<!-- app/views/comments/_form.html.erb -->
<%= simple_form_for([@shot, @shot.comments.build]) do |f| %>
<div class="field">
<div class="control">
<%= f.input :name, input_html: { class: 'input'}, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<div class="field">
<div class="control">
<%= f.input :response, input_html: { class: 'textarea' }, wrapper: false, label_html: { class: 'label' } %>
</div>
</div>
<%= f.button :submit, 'Leave a reply', class: 'button is-primary' %>
<% end %>
Это наша форма комментариев, которая отображается под самим кадром.
<!-- app/views/comments/_comment.html.erb -->
<div class="box">
<article class="media">
<figure class="media-left">
<p class="image is-48x48 user-thumb">
<%= gravatar_image_tag(comment.user.email.gsub('spam', 'mdeering'), alt: comment.user.name, gravatar: { size: 48 }) %>
</p>
</figure>
<div class="media-content">
<div class="content">
<p>
<strong><%= comment.user.name %></strong>
<div><%= comment.response %></div>
</p>
</div>
</div>
<% if (user_signed_in? && (current_user.id == comment.user_id)) %>
<%= link_to 'Delete', [comment.shot, comment],
method: :delete, class: "button is-danger", data: { confirm: 'Are you sure?' } %>
<% end %>
</article>
</div>
Это фактическая разметка комментария. Здесь я решаю получить имя вошедшего в систему пользователя, а не имя, которое они вводят в форме комментария. Если вы когда-нибудь хотели расширить это, чтобы пользователи, находящиеся в открытом доступе, могли комментировать, это, безусловно, возможно. На данный момент наш comments_controller.rb
требует от пользователя входа в систему или регистрации.
Наконец, отрисовка нашего комментария в файле show.html.erb
.
<!-- 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! -->
<div class="section">
<div class="container">
<h1 class="title is-3"><%= @shot.title %></h1>
<div class="columns">
<div class="column is-8">
<span class="by has-text-grey-light">by</span>
<div class="user-thumb">
<%= gravatar_image_tag(@shot.user.email.gsub('spam', 'mdeering'), alt: @shot.user.name, gravatar: { size: 20 }); %>
</div>
<div class="user-name has-text-weight-bold"><%= @shot.user.name %></div>
<div class="shot-time"><span class="has-text-grey-light">posted</span><span class="has-text-weight-semibold">
<%= verbose_date(@shot.created_at) %>
</span></div>
</div>
</div>
<div class="columns">
<div class="column is-8">
<div class="shot-container">
<div class="shot-full">
<%= image_tag @shot.user_shot_url unless @shot.user_shot.blank? %>
</div>
<% if user_signed_in? && (current_user.id == @shot.user_id) %>
<div class="buttons has-addons">
<%= link_to 'Edit', edit_shot_path(@shot), class: "button" %>
<%= link_to 'Delete', shot_path, class: "button", method: :delete, data: { confirm: 'Are you sure you want to delete this shot?'} %>
</div>
<% end %>
<div class="content">
<%= @shot.description %>
</div>
<section class="comments">
<h2 class="subtitle is-5"><%= pluralize(@shot.comments.count, 'Comment') %></h2>
<%= render @shot.comments %>
<hr />
<% if user_signed_in? %>
<div class="comment-form">
<h3 class="subtitle is-3">Leave a reply</h3>
<%= render 'comments/form' %>
</div>
<% else %>
<div class="content"><%= link_to 'Sign in', new_user_session_path %> to leave a comment.</div>
<% end %>
</section>
</div>
</div>
<div class="column is-3 is-offset-1">
<div class="nav panel show-shot-analytics">
<div class="panel-block views data">
<span class="icon"><i class="fa fa-eye"></i></span>
<%= pluralize(@shot.impressionist_count, 'View') %>
</div>
<div class="panel-block comments data">
<span class="icon"><i class="fa fa-comment"></i></span>
<%= pluralize(@shot.comments.count, 'Comment') %>
</div>
<div class="panel-block likes data">
<% if user_signed_in? %>
<% if current_user.liked? @shot %>
<%= link_to unlike_shot_path(@shot), method: :put, class: "unlike_shot" do %>
<span class="icon"><i class="fa fa-heart has-text-primary"></i></span>
<span class="vote_count"><%= pluralize(@shot.get_likes.size, 'Like') %></span>
<% end %>
<% else %>
<%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %>
<span class="icon"><i class="fa fa-heart"></i></span>
<span class="vote_count"><%= pluralize(@shot.get_likes.size, 'Like') %></span>
<% end %>
<% end %>
<% else %>
<%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %>
<span class="icon"><i class="fa fa-heart"></i></span>
<span class="vote_count"><%= pluralize(@shot.get_likes.size, 'Like') %></span>
<% end %>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
Импрессионистская установка
Получение уникальных просмотров на кадре — приятная функция. Чтобы облегчить себе задачу, я нашел жемчужину под названием Импрессионист. Это производит впечатление довольно легко.
$ rails g impressionist
$ rails db:migrate
Если есть ошибка при указании используемой версии rails, вам нужно добавить [5.1]
к миграции. Используйте любую версию рельсов, на которой вы находитесь.
Добавьте в файл shots_controller.rb
следующее:
class ShotsController < ApplicationController
# only recording when a user views the shot page and is unique
impressionist actions: [:show], unique: [:impressionable_type, :impressionable_id, :session_hash]
...
end
Добавьте в файл shot.rb в app/models/
следующее:
class Shot < 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
Отобразить его в представлениях:
<!-- on our shot index page app/views/shots/index.html.erb --> <%= shot.impressionist_count %>
<!-- on our shot show page app/views/shots/show.html.erb --> <%= @shot.impressionist_count %>
Добавление лайков с актами голосования
Еще одна приятная функция — возможность любить и не любить снимок. У Dribbble есть эта функция, хотя они немного используют AJAX за кулисами, чтобы все работало без обновления браузера.
Установка
Добавьте следующее в свой Gemfile, чтобы установить последнюю версию.
gem 'acts_as_votable', '~> 0.11.1' # check if this version is supported in your project
И за этим следует bundle install
.
Миграция базы данных
Acts As Votable использует таблицу голосов для хранения всей информации о голосовании. Для создания и запуска миграции используйте.
$ rails generate acts_as_votable:migration
$ rake db:migrate
Нам нужно добавить идентификатор acts_as_votable
к модели, за которую мы хотим получить один голос. В нашем случае это Shot
.
# app/models/shot.rb
class Shot < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
mount_uploader :user_shot, UserShotUploader
is_impressionable
acts_as_votable
end
Я также хочу определить пользователя как избирателя, поэтому мы добавляем acts_as_voter
в модель пользователя.
# app/models/user.rb
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 :shots
has_many :comments, dependent: :destroy
acts_as_voter
end
Создайте интерактивный лайк или отличие, нам нужно ввести некоторые маршруты в наше приложение. Ниже я добавил блок member do
, который, по сути, добавляет неуспокаивающие маршруты, вложенные в наши shots
умиротворяющие маршруты. Запустив здесь rake routes
, вы увидите каждый маршрут в приложении.
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
Контроллер нуждается в некоторой доработке, поэтому я добавил в смесь действия :like
и :unlike
. Они также передаются через наш before_actions
, который занимается поиском упоминаемого снимка, а также проверяет, что пользователь вошел в систему.
# 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
В нашем файле index.html.erb
внутри app/views/show/
мы добавляем в блок «нравится» следующее:
<!-- app/views/shots/index.html.erb -->
<div class="level-item likes data">
<div class="votes">
<% if user_signed_in? %>
<% if current_user.liked? shot %>
<%= link_to unlike_shot_path(shot), method: :put, class: 'unlike_shot' do %>
<span class="icon"><i class="fa fa-heart has-text-primary"></i></span>
<span class="vote_count"><%= shot.get_likes.size %></span>
<% end %>
<% else %>
<%= link_to like_shot_path(shot), method: :put, class: 'like_shot' do %>
<span class="icon"><i class="fa fa-heart"></i></span>
<span class="vote_count"><%= shot.get_likes.size %></span>
<% end %>
<% end %>
<% else %>
<%= link_to like_shot_path(shot), method: :put, class: 'like_shot' do %>
<span class="icon"><i class="fa fa-heart"></i></span>
<span class="vote_count"><%= shot.get_likes.size %></span>
<% end %>
<% end %>
</div>
</div>
А затем, чтобы повторить функциональность в файле show.html.erb
, мы добавляем следующее:
<!-- app/views/shots/show.html.erb -->
<div class="panel-block likes data">
<% if user_signed_in? %>
<% if current_user.liked? @shot %>
<%= link_to unlike_shot_path(@shot), method: :put, class: "unlike_shot" do %>
<span class="icon"><i class="fa fa-heart has-text-primary"></i></span>
<span class="vote_count"><%= pluralize(@shot.get_likes.size, 'Like') %></span>
<% end %>
<% else %>
<%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %>
<span class="icon"><i class="fa fa-heart"></i></span>
<span class="vote_count"><%= pluralize(@shot.get_likes.size, 'Like') %></span>
<% end %>
<% end %>
<% else %>
<%= link_to like_shot_path(@shot), method: :put, class: "like_shot" do %>
<span class="icon"><i class="fa fa-heart"></i></span>
<span class="vote_count"><%= pluralize(@shot.get_likes.size, 'Like') %></span>
<% end %>
<% end %>
</div>
Последние штрихи
На главной странице мы можем добавить немного маркетинга и отобразить общее место для людей, которые не вошли в систему. Если они вошли в систему, я не хочу, чтобы это отображалось. Конечный результат выглядит так с нашим индексным представлением:
<!-- app/views/shots/index.html.erb -->
<%= render 'hero' %> <!-- render the hero partial-->
<section class="section">
<div class="shots">
<% @shots.each do |shot| %>
...
В приведенном выше частичном у меня есть следующее. Мы проверяем, не вошел ли пользователь в систему. Если это не так, мы показываем баннер героя. Очень просто.
<% if !user_signed_in? %>
<section class="hero is-dark">
<div class="hero-body">
<div class="container has-text-centered">
<h1 class="title is-size-5">
What are you working on? <span class="has-text-grey-light">Dribbble is where designers get inspired and hired.</span>
</h1>
<div class="content">
<%= link_to "Login", new_user_session_path, class: "button is-primary" %>
</div>
</div>
</div>
</section>
<% end %>
Эффекты наведения
На индексной странице я хотел имитировать эффект наведения Dribbble на миниатюры снимков. Для этого требуется немного jQuery. Я добавил следующий метод в наш файл shots.js
, о котором я говорил ранее. Наш окончательный файл shots.scss
также приведен ниже для справки.
// app/assets/javascripts/shot.js ... }, shotHover() { $('.shot').hover(function() { $(this).children('.shot-data').toggleClass('visible'); }); } }; Shots.previewShot(); Shots.shotHover(); });
/* 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; }
Заключительные мысли и спасибо
Эта сборка была большим усилием, хотя снаружи она, вероятно, выглядит немного слабоватой. В Dribbble много движущихся частей, а наш клон лишь поверхностно поцарапал поверхность. Я приглашаю вас продолжать добавлять функции и интеграции там, где вы считаете нужным. Просто погружаться, ломать и исправлять вещи — это то, что в конечном итоге позволяет вам учиться и расти как веб-разработчик.
Спасибо, что заглянули в этот сериал. Если вы следили за новостями или у вас есть отзывы, дайте мне знать в комментариях или в Твиттере. Если вы еще этого не сделали, обязательно ознакомьтесь с остальной частью блога, чтобы узнать больше статей о дизайне и разработке.
Посмотрите/прочитайте полную серию Ruby on Rails Let’s Build:
- Введение в Ruby on Rails
- Установка Ruby on Rails на вашу машину
- Создайте простой блог с комментариями, используя Ruby on Rails
- Создайте клон Twitter с помощью Ruby on Rails
Первоначально опубликовано на веб-сайте web-crunch.com 18 декабря 2017 г.
☝ Хотите изучить Ruby on Rails с нуля? Ознакомьтесь с моим предстоящим курсом под названием Hello Rails.