Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Comment votes #26

Merged
merged 8 commits into from
Jul 1, 2024
Merged
73 changes: 42 additions & 31 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
document.addEventListener("DOMContentLoaded", function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const upvoteButtons = document.querySelectorAll('.upvote-button');
const downvoteButtons = document.querySelectorAll('.downvote-button');

// Function to handle voting
function handleVote(articleId, path) {
fetch(`/articles/${articleId}/${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
document.getElementById(`score-${articleId}`).textContent = data.new_score;
})
.catch(error => console.log('Error:', error));
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const upvoteButtons = document.querySelectorAll('.upvote-button');
const downvoteButtons = document.querySelectorAll('.downvote-button');

// Function to handle voting
function handleVote(votableId, votableType, path, articleId = null) {
let url;
if (votableType === 'Article') {
url = `/articles/${votableId}/${path}`;
} else if (votableType === 'Comment') {
url = `/articles/${articleId}/comments/${votableId}/${path}`;
}
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({})
})
.then(response => response.json())
.then(data => {
document.getElementById(`score-${votableType}-${votableId}`).textContent = data.new_score;
})
.catch(error => console.log('Error:', error));
}

upvoteButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const articleId = this.dataset.articleId;
handleVote(articleId, 'upvote');
});
upvoteButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const votableId = this.dataset.votableId;
const votableType = this.dataset.votableType;
const articleId = this.dataset.articleId;
handleVote(votableId, votableType, 'upvote');
});
});

downvoteButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const articleId = this.dataset.articleId;
handleVote(articleId, 'downvote');
});
downvoteButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
const votableId = this.dataset.votableId;
const votableType = this.dataset.votableType;
const articleId = this.dataset.articleId;
handleVote(votableId, votableType, 'downvote', articleId);
});
});
});

48 changes: 41 additions & 7 deletions app/controllers/comments_controller.rb
Original file line number Diff line number Diff line change
@@ -1,27 +1,61 @@
class CommentsController < ApplicationController
before_action :authorized, only: [:create]
helper_method :comment

def show
@comment = Comment.find(params[:id])
@reply = Comment.new
@user_votes = Vote.where(user: current_user, votable: @comments.to_a).index_by(&:votable_id)
end

def upvote
vote(1)
respond_to do |format|
format.json { render json: {new_score: comment.score} }
end
end

def downvote
vote(-1)
respond_to do |format|
format.json { render json: {new_score: comment.score} }
end
end

def create
@article = Article.find(params[:article_id])
@comment = @article.comments.new(comment_params)
@comment.user = current_user
@comment = article.comments.new(comment_params)
comment.user = current_user

if @comment.save
redirect_to @article
if comment.save
if comment.parent_id.present?
redirect_to article_comment_path(article, comment.parent_id)
else
redirect_to article
end
else
@comments = @article.comments.order(created_at: :desc)
render "articles/show", status: :unprocessable_entity
end
end

private

def article
@article ||= Article.find(params[:article_id])
end

def comment
@comment ||= Comment.find(params[:id])
end

def comment_params
params.require(:comment).permit(:text, :parent_id)
end

def vote(value)
vote = comment.votes.find_or_initialize_by(user: current_user)
if vote.value == value
vote.update(value: 0)
else
vote.update(value: value)
end
end
end
9 changes: 7 additions & 2 deletions app/helpers/comments_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@ module CommentsHelper
def render_comment_tree(comment)
content_tag :li do
concat content_tag(:p, comment.text)
concat content_tag(:p) {
concat content_tag(:div, style: "display: flex; align-items: center;") {
safe_concat "Posted by "
safe_concat link_to(comment.user.name, comment.user)
safe_concat " on #{comment.created_at.strftime("%B %d, %Y at %I:%M %p")}"
safe_concat " on #{comment.created_at.strftime("%B %d, %Y at %I:%M %p")}"
safe_concat " - Score: "
safe_concat content_tag(:span, comment.score, id: "score-Comment-#{comment.id}")

safe_concat button_to("Upvote", upvote_article_comment_path(comment.article, comment), method: :post, class: "btn btn-sm upvote-button", data: {votable_type: "Comment", votable_id: comment.id, article_id: comment.article.id})
safe_concat button_to("Downvote", downvote_article_comment_path(comment.article, comment), method: :post, class: "btn btn-sm downvote-button", data: {votable_type: "Comment", votable_id: comment.id, article_id: comment.article.id})
}
concat content_tag(:p) {
safe_concat link_to("reply", article_comment_path(comment.article, comment))
Expand Down
4 changes: 4 additions & 0 deletions app/models/comment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ class Comment < ApplicationRecord
belongs_to :parent, class_name: "Comment", optional: true
has_many :replies, class_name: "Comment", foreign_key: :parent_id, dependent: :destroy
scope :without_parent, -> { where(parent_id: nil) }

def score
votes.sum(:value)
end
end
6 changes: 3 additions & 3 deletions app/views/articles/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
<li>
<div style="display: flex; align-items: center;">
<%= link_to article.title, article.link, target: "_blank" %>
- Score: <span id="score-<%= article.id %>"><%= article.score %></span>
- Score: <span id="score-Article-<%= article.id %>"><%= article.score %></span>
<% user_vote = @user_votes[article.id] %>
<%= button_to "Upvote", upvote_article_path(article), method: :post, class: "btn btn-sm upvote-button", data: { article_id: article.id } %>
<%= button_to "Upvote", upvote_article_path(article), method: :post, class: "btn btn-sm upvote-button", data: { votable_type: 'Article', votable_id: article.id } %>

<%= button_to "Downvote", downvote_article_path(article), method: :post, method: :post, class: "btn btn-sm downvote-button", data: { article_id: article.id } %>
<%= button_to "Downvote", downvote_article_path(article), method: :post, method: :post, class: "btn btn-sm downvote-button", data: { votable_type: 'Article', votable_id: article.id } %>
</div>
- Posted by <%= link_to article.user.name, user_path(article.user) %> on <%= article.created_at.strftime("%B %d, %Y at %I:%M %p") %>
<div>
Expand Down
19 changes: 12 additions & 7 deletions app/views/comments/show.html.erb
paulmckissock marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
<h1>Comment by <%= link_to @comment.user.name, @comment.user %></h1>
<p><%= @comment.text %></p>
<p>Posted on <%= @comment.created_at.strftime("%B %d, %Y at %I:%M %p") %></p>

<h1>Comment by <%= link_to comment.user.name, comment.user %></h1>
<p><%= comment.text %></p>
<div style="display: flex; align-items: center;">
Posted on <%= comment.created_at.strftime("%B %d, %Y at %I:%M %p") %>
- Score: <span id="score-Comment-<%= comment.id %>"><%= comment.score %></span>
<% user_vote = @user_votes[comment.id] %>
<%= button_to "Upvote", upvote_article_comment_path(comment), method: :post, class: "btn btn-sm upvote-button", data: { votable_type: 'Comment', votable_id: comment.id, article_id: comment.article.id } %>
<%= button_to "Downvote", downvote_article_comment_path(comment), method: :post, method: :post, class: "btn btn-sm downvote-button", data: { votable_type: 'Comment', votable_id: comment.id, article_id: comment.article.id } %>
</div>
<h2>Replies</h2>
<ul>
<% @comment.replies.each do |reply| %>
<% comment.replies.each do |reply| %>
<%= render_comment_tree(reply) %>
<% end %>
</ul>

<h3>Write a Reply</h3>
<%= form_with(model: [@comment.article, @reply], local: true) do |form| %>
<%= form_with(model: [comment.article, @reply], local: true) do |form| %>
<div class="field">
<%= form.text_area :text %>
</div>
<%= form.hidden_field :parent_id, value: @comment.id %>
<%= form.hidden_field :parent_id, value: comment.id %>
<div class="actions">
<%= form.submit "Reply" %>
</div>
Expand Down
8 changes: 7 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
Rails.application.routes.draw do
root "articles#index"
resources :articles do
resources :comments, only: [:create, :show]
member do
post "upvote"
post "downvote"
end

resources :comments, only: [:create, :show] do
member do
post "upvote"
post "downvote"
end
end
end
resources :users, only: [:create, :show]
get "/signup", to: "users#new"
Expand Down
30 changes: 26 additions & 4 deletions db/seeds.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,27 @@
end

comments = Comment.all
replies = []
# Create replies to comments
90.times do |i|
comment = comments.sample
Comment.create!(
text: "Sample Reply #{i + 1}",
comments.each do |comment|
replies << Comment.create!(
text: "Sample Reply",
article: comment.article,
user: users.sample,
parent_id: comment.id
)
end

# replies to replies
replies.each do |reply|
Comment.create!(
text: "Sample Reply",
article: reply.article,
user: users.sample,
parent_id: reply.id
)
end

# Generate votes for articles
articles.each do |article|
users.each do |user|
Expand All @@ -61,3 +72,14 @@
)
end
end
comments = Comment.all
# Generate votes for comments
comments.each do |comment|
users.each do |user|
Vote.create!(
votable: comment,
user: user,
value: [-1, 0, 1].sample
)
end
end
35 changes: 35 additions & 0 deletions spec/controllers/comments_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
RSpec.describe CommentsController, type: :controller do
let(:user) { create(:user) }
let(:article) { create(:article, user: user) }
let(:comment) { create(:comment, user: user, article: article) }
let(:parent_comment) { create(:comment, article: article, user: user) }
let(:comment_params) { {text: "This is a test comment"} }
let(:reply_params) { {text: "This is a test reply", parent_id: parent_comment.id} }
Expand Down Expand Up @@ -52,5 +53,39 @@
}
expect(response.code).to eq("422")
end

describe "POST #upvote" do
it "sets vote value to 1 if user has not voted" do
expect {
post :upvote, params: {article_id: article.id, id: comment.id}, format: :json
}.to change { comment.votes.count }.by(1)
expect(comment.reload.votes.last.value).to eq(1)
end

it "sets vote value to 0 if user has already upvoted" do
create(:vote, votable: comment, user: user, value: 1)
expect {
post :upvote, params: {article_id: article.id, id: comment.id}, format: :json
}.not_to change { comment.votes.count }
expect(comment.reload.votes.last.value).to eq(0)
end
end

describe "POST #downvote" do
it "sets vote value to -1 if user has not already downvoted" do
expect {
post :downvote, params: {article_id: article.id, id: comment.id}, format: :json
}.to change { comment.votes.count }.by(1)
expect(comment.reload.votes.last.value).to eq(-1)
end

it "sets vote value to 0 if user has already downvoted" do
create(:vote, votable: comment, user: user, value: -1)
expect {
post :downvote, params: {article_id: article.id, id: comment.id}, format: :json
}.not_to change { comment.votes.count }
expect(comment.reload.votes.last.value).to eq(0)
end
end
end
end