diff --git a/Gemfile b/Gemfile index 66c0587..2fa449b 100644 --- a/Gemfile +++ b/Gemfile @@ -79,3 +79,5 @@ group :test do end gem 'devise', '~> 4.9' + +gem 'cancancan', '~> 3.5' diff --git a/Gemfile.lock b/Gemfile.lock index a3e56bc..ddb4ff8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,6 +74,7 @@ GEM bootsnap (1.16.0) msgpack (~> 1.2) builder (3.2.4) + cancancan (3.5.0) capybara (3.39.2) addressable matrix @@ -268,6 +269,7 @@ PLATFORMS DEPENDENCIES bootsnap + cancancan (~> 3.5) capybara debug devise (~> 4.9) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 218a5a1..2fbaefe 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -4,14 +4,39 @@ box-sizing: border-box; } +.wrapper { + margin: 1rem auto; + gap: 1rem; + box-shadow: 0 0 2px 2px rgb(231, 231, 237); +} + +.wrapper, .container { - width: 80vw; + width: 90vw; height: 100vh; display: flex; flex-direction: column; align-items: center; - margin: 2rem 0; + background-color: whitesmoke; +} + +.navbar { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; gap: 1rem; + padding: 1rem; + background-color: #1868f1; + border: 1px solid rgb(128, 128, 128); +} + +.logout { + cursor: pointer; + padding: 5px 10px; + font-size: 15px; + font-weight: bold; } .user { @@ -21,6 +46,7 @@ justify-content: center; align-items: center; gap: 2rem; + padding: 1rem; } .user-photo { @@ -36,7 +62,7 @@ } .user-info { - width: 46%; + width: 80%; display: flex; flex-direction: row; justify-content: space-between; @@ -51,7 +77,9 @@ } .user-posts { - justify-content: flex-end; + display: flex; + flex-direction: column; + gap: 1rem; align-self: flex-end; padding: 2rem 1rem 1rem 1rem; font-size: small; @@ -70,10 +98,11 @@ a { } .post { - width: 60%; + width: 90%; display: flex; flex-direction: column; border: 3px solid black; + margin-bottom: 1rem; } .post h3 { @@ -86,42 +115,82 @@ a { padding: 0.5rem 1rem; } +.post-text { + display: grid; + grid-template-columns: 2fr 0.5fr; + gap: 1rem; +} + .count { display: flex; flex-direction: row; - justify-content: flex-end; + justify-content: space-around; + align-items: center; padding: 0.5rem 1rem; - font-size: small; + font-size: medium; + gap: 0.5rem; +} + +.post-text .count { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: flex-start; + text-align: center; + padding: 0.5rem 2rem 0.5rem 10vh; + font-size: medium; + gap: 0.5rem; } .count span { padding: 0 0.25rem; } +.like_count_btn { + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; +} + .post-comments { - width: 60%; + width: 100%; display: flex; flex-direction: column; padding: 1rem; gap: 0.5rem; - border: 3px solid black; } .btn-link { + width: auto; align-self: center; } -button { +button, +form input[type="submit"] { border-bottom: 5px solid black; border-right: 3px solid black; border-radius: 5px; + padding: 5px; +} + +.like_count_btn button { + width: 2vw; + height: 2vw; + font-size: x-large; + padding: 0; +} + +form input[type="hidden"] { + width: 1px; + height: 1px; } .btn-link, .btn-link button { cursor: pointer; - padding: 5px; font-size: 15px; + margin: 10px 0; } .post-form, @@ -135,21 +204,39 @@ button { background-color: #f5f5f5; } -.comments, .posts { width: 80%; display: flex; - flex-direction: column; + flex-direction: row; padding: 1rem; gap: 1rem; } +.comments { + display: grid; + grid-template-columns: 0.5fr 2.5fr 0.5fr; +} + +.post .add-comment { + margin: 0 auto; + align-self: center; +} + +.comment-form textarea, +.post-form textarea { + width: 100%; + height: 100px; + padding: 0.5rem; + border: 1px solid black; + border-radius: 5px; +} + .comment-btn, .like-button { + width: auto; cursor: pointer; padding: 5px 10px; margin-left: 5rem; - font-size: 15px; border-bottom: 5px solid black; border-right: 3px solid black; border-radius: 5px; @@ -184,6 +271,15 @@ form input { padding: 0.5rem; } +.like_count_btn form { + width: 3vw; +} + +.comments form { + width: auto; + align-self: flex-end; +} + .shared { display: flex; flex-direction: column-reverse; @@ -226,7 +322,7 @@ ul { .auth_links { display: flex; - flex-direction: row; + flex-direction: column; justify-content: flex-start; align-items: center; gap: 1rem; @@ -234,6 +330,12 @@ ul { margin: auto auto; } +.sign_in_up { + display: flex; + flex-direction: row; + gap: 1rem; +} + .auth_links a { color: blue; padding: 0.5rem; @@ -284,8 +386,44 @@ ul { } .form-elements { + width: 100%; + display: flex; + flex-direction: row; + align-items: center; + gap: 1rem; + margin: 1rem 0; +} + +.like-button { + border: none; + background-color: transparent; + font-size: 2rem; +} + +.short-button { + width: 7vw; + align-items: flex-end; +} + +.user-comments-likes { display: flex; flex-direction: row; + justify-content: flex-end; + align-items: center; gap: 1rem; + width: 100%; + margin: 1rem 0; +} + +.count form { + width: auto; +} + +.post .count { + display: grid; + grid-template-columns: 0.5fr 0.5fr; + justify-items: end; + gap: 1rem; + width: 100%; margin: 1rem 0; } diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index cf3ebcb..fc1d81a 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -18,6 +18,18 @@ def create end end + def destroy + @comment = Comment.find(params[:id]) + @post = @comment.post + + if can? :destroy, @comment + @comment.destroy + redirect_to user_post_path(@post.author, @post), notice: 'Comment was successfully deleted.' + else + redirect_to user_posts_path(current_user), alert: 'You are not authorized to delete this comment.' + end + end + private def comment_params diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 54c250d..caee4ac 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -1,4 +1,6 @@ class PostsController < ApplicationController + load_and_authorize_resource + def index @user = User.includes(posts: { comments: :author }).find(params[:user_id]) @current_user = current_user @@ -27,6 +29,17 @@ def create end end + def destroy + @post = Post.find(params[:id]) + + if can? :destroy, @post + @post.destroy + redirect_to "/users/#{current_user.id}/posts", notice: 'Successfully deleted.' + else + redirect_to user_post_path(@post.author_id, @post), alert: 'Unauthorized action.' + end + end + private def post_params diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000..1ccac09 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,38 @@ +class Ability + include CanCan::Ability + + def initialize(user) + # Define abilities for the user here. For example: + can :read, :all + + return unless user.present? + + can :manage, User, id: user.id # user can manage only his own profile + can :manage, Post, author_id: user.id # user can manage only his own posts + can :manage, Comment, user_id: user.id # user can manage only his own comments + can :create, Like # user can create likes + + return unless user.role == 'admin' + + can :destroy, Post # admin can delete any post + can :destroy, Comment # admin can delete any comment + + # The first argument to `can` is the action you are giving the user + # permission to do. + # If you pass :manage it will apply to every action. Other common actions + # here are :read, :create, :update and :destroy. + # + # The second argument is the resource the user can perform the action on. + # If you pass :all it will apply to every resource. Otherwise pass a Ruby + # class of the resource. + # + # The third argument is an optional hash of conditions to further filter the + # objects. + # For example, here the user can only update published articles. + # + # can :update, Article, published: true + # + # See the wiki for details: + # https://github.com/CanCanCommunity/cancancan/blob/develop/docs/define_check_abilities.md + end +end diff --git a/app/models/user.rb b/app/models/user.rb index 9605970..3609298 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,8 +1,9 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :database_authenticatable, :registerable, + devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :validatable + has_many :posts, foreign_key: :author_id, dependent: :destroy has_many :comments, foreign_key: :user_id, dependent: :destroy has_many :likes, foreign_key: :user_id, dependent: :destroy diff --git a/app/views/comments/new.html.erb b/app/views/comments/new.html.erb index 5a14028..8dbecb3 100644 --- a/app/views/comments/new.html.erb +++ b/app/views/comments/new.html.erb @@ -2,9 +2,7 @@
Add Comment
<%= form_with(model: @comment, url: user_post_comments_url(@post.author, @post), local: true) do |form| %> -- <%= f.text_field :name, autofocus: true, placeholder: "Name" %> + <%= f.text_field :name, autofocus: true, placeholder: "Full Name" %>
- <%= f.email_field :email, autofocus: true, autocomplete: "email" %> + <%= f.email_field :email, autofocus: true, autocomplete: "email", placeholder: "Email address" %>
- <%= f.password_field :password, autocomplete: "new-password" %> + <%= f.password_field :password, autocomplete: "new-password", placeholder: "New Password" %>
- <%= f.password_field :password_confirmation, autocomplete: "new-password" %> + <%= f.password_field :password_confirmation, autocomplete: "new-password", placeholder: "Confirm Password" %>
- <% end %> + <% end %> - <%- if devise_mapping.registerable? && controller_name != 'registrations' %> - <%= link_to "Sign up", new_registration_path(resource_name) %>
- <% end %> + <%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+ <% end %> - <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> - <%= link_to "Forgot your password?", new_password_path(resource_name) %>
- <% end %> + <%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+ <% end %> +
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5fd34af..af8cf4c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -11,19 +11,28 @@ -
<% if user_signed_in? %> - Logged in as <%= current_user.name %>. - <%= link_to "Edit profile", edit_user_registration_path, class: "navbar-link" %> | - <%= link_to "Logout", destroy_user_session_path, data: { "turbo-method": :delete }, class: "navbar-link" %> - <% end %> -
- <% if notice %> -<%= notice %>
- <% end %> +<%= notice %>
+ <% end %> - - <%= yield %> + <%= yield %> +