Merge pull request #1403 from Retrospring/feature/avatar-component

Install `view_component` and add initial `AvatarComponent`
This commit is contained in:
Karina Kwiatek 2023-10-22 16:05:11 +02:00 committed by GitHub
commit 2432010256
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 108 additions and 20 deletions

View File

@ -27,6 +27,10 @@ Lint/NestedMethodDefinition:
Exclude: Exclude:
- api/sinatra/**/* - api/sinatra/**/*
Lint/MissingSuper:
Exclude:
- app/components/**/*
### Metrics ### Metrics

View File

@ -66,6 +66,8 @@ gem "fake_email_validator"
# TLD validation # TLD validation
gem "tldv", "~> 0.1.0" gem "tldv", "~> 0.1.0"
gem "view_component"
gem "jwt", "~> 2.7" gem "jwt", "~> 2.7"
group :development do group :development do

View File

@ -485,6 +485,10 @@ GEM
unf_ext (0.0.8) unf_ext (0.0.8)
unicode-display_width (2.5.0) unicode-display_width (2.5.0)
uniform_notifier (1.16.0) uniform_notifier (1.16.0)
view_component (3.6.0)
activesupport (>= 5.2.0, < 8.0)
concurrent-ruby (~> 1.0)
method_source (~> 1.0)
warden (1.2.9) warden (1.2.9)
rack (>= 2.0.9) rack (>= 2.0.9)
webpush (1.1.0) webpush (1.1.0)
@ -580,6 +584,7 @@ DEPENDENCIES
tldv (~> 0.1.0) tldv (~> 0.1.0)
turbo-rails turbo-rails
twitter-text twitter-text
view_component
BUNDLED WITH BUNDLED WITH
2.4.21 2.4.21

View File

@ -0,0 +1,4 @@
%img{ class: avatar_classes,
alt: alt_text,
src: avatar_image,
loading: :lazy }

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class AvatarComponent < ViewComponent::Base
ALLOWED_SIZES = %w[xs sm md lg xl xxl].freeze
def initialize(user:, size:, classes: [])
@user = user
@size = size if ALLOWED_SIZES.include? size
@classes = classes
end
private
def size_to_version(size)
case size
when "xs", "sm"
:small
when "md", "lg"
:medium
when "xl", "xxl"
:large
end
end
def alt_text = "@#{@user.screen_name}"
def avatar_classes = @classes.unshift("avatar-#{@size}")
def avatar_image = @user.profile_picture.url(size_to_version(@size))
end

View File

@ -8,7 +8,7 @@
.d-flex .d-flex
.flex-shrink-0 .flex-shrink-0
%a{ href: user_path(comment.user) } %a{ href: user_path(comment.user) }
%img.comment__user-avatar.avatar-sm{ src: comment.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: comment.user, size: "sm", classes: ["comment__user-avatar"])
.flex-grow-1 .flex-grow-1
%h6.comment__user %h6.comment__user
= user_screen_name comment.user = user_screen_name comment.user

View File

@ -3,7 +3,7 @@
- unless a.question.author_is_anonymous - unless a.question.author_is_anonymous
.flex-shrink-0 .flex-shrink-0
%a{ href: user_path(a.question.user) } %a{ href: user_path(a.question.user) }
%img.answerbox__question-user-avatar.avatar-md{ src: a.question.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: a.question.user, size: "md", classes: ["answerbox__question-user-avatar"])
.flex-grow-1 .flex-grow-1
%h6.text-muted.answerbox__question-user %h6.text-muted.answerbox__question-user
- if a.question.author_is_anonymous - if a.question.author_is_anonymous

View File

@ -9,4 +9,4 @@
%a{ href: user_path(smile.user), %a{ href: user_path(smile.user),
title: user_screen_name(smile.user, url: false), title: user_screen_name(smile.user, url: false),
data: { bs_toggle: :tooltip, bs_placement: :top, smile_id: smile.id } } data: { bs_toggle: :tooltip, bs_placement: :top, smile_id: smile.id } }
%img.avatar-xs{ src: smile.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: smile.user, size: "xs")

View File

@ -14,7 +14,7 @@
.d-flex .d-flex
.flex-shrink-0 .flex-shrink-0
%a{ href: user_path(a.user) } %a{ href: user_path(a.user) }
%img.answerbox__answer-user-avatar.avatar-sm{ src: a.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: a.user, size: "sm", classes: ["answerbox__answer-user-avatar"])
.flex-grow-1 .flex-grow-1
%h6.answerbox__answer-user %h6.answerbox__answer-user
= raw t(".answered", hide: hidespan(t(".hide"), "d-none d-sm-inline"), user: user_screen_name(a.user)) = raw t(".answered", hide: hidespan(t(".hide"), "d-none d-sm-inline"), user: user_screen_name(a.user))

View File

@ -3,7 +3,7 @@
.d-flex .d-flex
.flex-shrink-0 .flex-shrink-0
%a{ href: user_path(u) } %a{ href: user_path(u) }
%img.avatar-md.me-2{ src: u.profile_picture.url(:medium) } = render AvatarComponent.new(user: u, size: "md", classes: ["me-2"])
.flex-grow-1 .flex-grow-1
%h6.answerbox__question-user %h6.answerbox__question-user
- if u.profile.display_name.blank? - if u.profile.display_name.blank?

View File

@ -4,7 +4,7 @@
- unless i.question.author_is_anonymous - unless i.question.author_is_anonymous
.flex-shrink-0 .flex-shrink-0
%a.pull-left{ href: user_path(i.question.user) } %a.pull-left{ href: user_path(i.question.user) }
%img.answerbox__question-user-avatar.avatar-md{ src: i.question.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: i.question.user, size: "md", classes: ["answerbox__question-user-avatar"])
.flex-grow-1 .flex-grow-1
%h6.text-muted.answerbox__question-user %h6.text-muted.answerbox__question-user
- if i.question.author_is_anonymous - if i.question.author_is_anonymous

View File

@ -1,6 +1,6 @@
.card.moderationbox{ data: { id: report.id } } .card.moderationbox{ data: { id: report.id } }
.card-header .card-header
%img.avatar-sm{ src: report.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: report.user, size: "sm")
= t(".reported_html", = t(".reported_html",
user: user_screen_name(report.user), user: user_screen_name(report.user),
content: report.type.sub("Reports::", ""), content: report.type.sub("Reports::", ""),

View File

@ -4,6 +4,6 @@
.d-flex .d-flex
.flex-shrink-0 .flex-shrink-0
%a{ href: user_path(user) } %a{ href: user_path(user) }
%img.answerbox__question-user-avatar.avatar-md{ src: user.profile_picture.url(:medium) } = render AvatarComponent.new(user:, size: "md", classes: ["answerbox__question-user-avatar"])
.flex-grow-1 .flex-grow-1
= t(".title_html", screen_name: user.screen_name, user_id: user.id) = t(".title_html", screen_name: user.screen_name, user_id: user.id)

View File

@ -34,7 +34,7 @@
%i.fa.fa-pencil-square-o %i.fa.fa-pencil-square-o
%li.nav-item.dropdown.profile--image-dropdown %li.nav-item.dropdown.profile--image-dropdown
%a.nav-link.dropdown-toggle.p-sm-0{ href: "#", data: { bs_toggle: :dropdown } } %a.nav-link.dropdown-toggle.p-sm-0{ href: "#", data: { bs_toggle: :dropdown } }
%img.avatar-md.d-none.d-sm-inline{ src: current_user.profile_picture.url(:small) } = render AvatarComponent.new(user: current_user, size: "md", classes: ["d-none", "d-sm-inline"])
%span.d-inline.d-sm-none %span.d-inline.d-sm-none
= current_user.screen_name = current_user.screen_name
%b.caret %b.caret

View File

@ -12,5 +12,5 @@
badge: notification_count, badge_color: "primary", badge_attr: { id: "notification-mobile-count" }, icon_only: true badge: notification_count, badge_color: "primary", badge_attr: { id: "notification-mobile-count" }, icon_only: true
%li.nav-item.profile--image-dropdown %li.nav-item.profile--image-dropdown
%a.nav-link{ href: '#', data: { bs_toggle: 'dropdown', bs_target: '#rs-mobile-nav-profile' }, aria: { controls: 'rs-mobile-nav-profile', expanded: 'false' } } %a.nav-link{ href: '#', data: { bs_toggle: 'dropdown', bs_target: '#rs-mobile-nav-profile' }, aria: { controls: 'rs-mobile-nav-profile', expanded: 'false' } }
%img.avatar-md.d-inline{ src: current_user.profile_picture.url(:small) } = render AvatarComponent.new(user: current_user, size: "md", classes: ["d-inline"])
= render 'navigation/dropdown/profile', size: "mobile" = render 'navigation/dropdown/profile', size: "mobile"

View File

@ -3,7 +3,7 @@
%i.fa.fa-2x.fa-fw.fa-exclamation %i.fa.fa-2x.fa-fw.fa-exclamation
.flex-grow-1 .flex-grow-1
.notification__heading .notification__heading
%img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: notification.target.user, size: "xs")
= t(".heading_html", = t(".heading_html",
user: user_screen_name(notification.target.user), user: user_screen_name(notification.target.user),
question: link_to(t(".link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.id), target: "_top"), question: link_to(t(".link_text"), answer_path(username: notification.target.user.screen_name, id: notification.target.id), target: "_top"),

View File

@ -3,7 +3,7 @@
%i.fa.fa-2x.fa-fw.fa-comments %i.fa.fa-2x.fa-fw.fa-comments
.flex-grow-1 .flex-grow-1
.notification__heading .notification__heading
%img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: notification.target.user, size: "xs")
- if notification.target.answer.user == current_user - if notification.target.answer.user == current_user
= t(".heading_html", = t(".heading_html",
user: user_screen_name(notification.target.user), user: user_screen_name(notification.target.user),

View File

@ -1,6 +1,6 @@
.d-flex.notification .d-flex.notification
.flex-shrink-0.notification__icon .flex-shrink-0.notification__icon
%img.avatar-sm{ src: notification.target.source.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: notification.target.source, size: "sm")
.flex-grow-1 .flex-grow-1
%h6.notification__user %h6.notification__user
= user_screen_name notification.target.source = user_screen_name notification.target.source

View File

@ -3,7 +3,7 @@
%i.fa.fa-2x.fa-fw.fa-smile-o %i.fa.fa-2x.fa-fw.fa-smile-o
.flex-grow-1 .flex-grow-1
.notification__heading .notification__heading
%img.avatar-xs{ src: notification.target.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: notification.target.user, size: "xs")
- if notification.target.parent_type == "Answer" - if notification.target.parent_type == "Answer"
= t(".heading_html", = t(".heading_html",
user: user_screen_name(notification.target.user), user: user_screen_name(notification.target.user),

View File

@ -5,7 +5,7 @@
- unless question.author_is_anonymous - unless question.author_is_anonymous
.flex-shrink-0 .flex-shrink-0
%a{ href: unless hidden then user_path(question.user) end } %a{ href: unless hidden then user_path(question.user) end }
%img.answerbox__question-user-avatar.avatar-md{ src: question.user.profile_picture.url(:small) } = render AvatarComponent.new(user: question.user, size: "md", classes: ["answerbox__question-user-avatar"])
.flex-grow-1 .flex-grow-1
%h6.text-muted.answerbox__question-user %h6.text-muted.answerbox__question-user
- identifier = question.author_is_anonymous ? question.author_identifier : nil - identifier = question.author_is_anonymous ? question.author_identifier : nil

View File

@ -6,7 +6,7 @@
- @blocks.each do |block| - @blocks.each do |block|
%li.list-group-item %li.list-group-item
.d-flex .d-flex
%img.avatar-md.d-none.d-sm-inline.me-2{ src: block.target.profile_picture.url(:small) } = render AvatarComponent.new(user: block.target, size: "md", classes: ["d-none", "d-sm-inline", "me-2"])
%div %div
%p.mb-0= user_screen_name(block.target) %p.mb-0= user_screen_name(block.target)
%p.text-muted.mb-0= t(".blocked", time: time_ago_in_words(block.created_at)) %p.text-muted.mb-0= t(".blocked", time: time_ago_in_words(block.created_at))

View File

@ -1,5 +1,5 @@
.d-flex.mb-2 .d-flex.mb-2
%img.avatar-md.me-2{ src: user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user:, size: "md", classes: ["me-2"])
%p.align-self-center.m-0= user_screen_name(user, context_user: current_user) %p.align-self-center.m-0= user_screen_name(user, context_user: current_user)
.ms-auto.d-inline-flex .ms-auto.d-inline-flex
%button.btn.btn-default.align-self-center{ data: { action: :unmute, target: user.screen_name } } %button.btn.btn-default.align-self-center{ data: { action: :unmute, target: user.screen_name } }

View File

@ -5,7 +5,7 @@
%div{ data: { controller: "cropper", cropper_aspect_ratio_value: "1" } } %div{ data: { controller: "cropper", cropper_aspect_ratio_value: "1" } }
.d-flex .d-flex
.flex-shrink-0 .flex-shrink-0
%img.avatar-lg.me-3{ src: current_user.profile_picture.url(:medium) } = render AvatarComponent.new(user: current_user, size: "lg", classes: ["me-3"])
.flex-grow-1 .flex-grow-1
= f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" } = f.file_field :profile_picture, accept: APP_CONFIG[:accepted_image_formats].join(","), data: { cropper_target: "input", action: "cropper#change" }

View File

@ -5,7 +5,7 @@
- if type == "discover" - if type == "discover"
.flex-shrink-0 .flex-shrink-0
%a{ href: user_screen_name(q.user, link_only: true) } %a{ href: user_screen_name(q.user, link_only: true) }
%img.avatar-md.me-2{ src: q.user&.profile_picture&.url(:small), loading: :lazy } = render AvatarComponent.new(user: q.user, size: "md", classes: ["me-2"])
.flex-grow-1 .flex-grow-1
%h6.text-muted.answerbox__question-user %h6.text-muted.answerbox__question-user
- if type.nil? && q.direct - if type.nil? && q.direct

View File

@ -18,7 +18,7 @@
%p.px-4.pb-2 %p.px-4.pb-2
- list.members.each do |member| - list.members.each do |member|
%a{ href: user_path(member.user), title: member.user.screen_name, data: { bs_toggle: :tooltip, bs_placement: :top } } %a{ href: user_path(member.user), title: member.user.screen_name, data: { bs_toggle: :tooltip, bs_placement: :top } }
%img.avatar-xs{ src: member.user.profile_picture.url(:small), loading: :lazy } = render AvatarComponent.new(user: member.user, size: "xs")
- if !list && lists.empty? - if !list && lists.empty?
.p-3= t(".lists.notice_html") .p-3= t(".lists.notice_html")
- lists.each do |list| - lists.each do |list|

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require "rails_helper"
RSpec.describe AvatarComponent, type: :component do
let(:user) { FactoryBot.create(:user) }
it "renders an avatar" do
expect(
render_inline(described_class.new(user:, size: "sm")).to_html,
).to include(
"no_avatar.png",
)
end
it "gets the medium version of a profile picture if requested" do
expect(
render_inline(described_class.new(user:, size: "md")).to_html,
).to include(
"medium/",
)
end
it "gets the large version of a profile picture if requested" do
expect(
render_inline(described_class.new(user:, size: "xl")).to_html,
).to include(
"large/",
)
end
it "includes additionally passed classes" do
expect(
render_inline(described_class.new(user:, size: "md", classes: %w[first-class second-class])).to_html,
).to include(
'class="avatar-md first-class second-class"',
)
end
end

View File

@ -11,6 +11,8 @@ require "rspec/rails"
require "rspec/its" require "rspec/its"
require "devise" require "devise"
require "rspec-sidekiq" require "rspec-sidekiq"
require "view_component/test_helpers"
require "view_component/system_test_helpers"
# Requires supporting ruby files with custom matchers and macros, etc, in # Requires supporting ruby files with custom matchers and macros, etc, in
# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
@ -68,6 +70,8 @@ RSpec.configure do |config|
config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :helper config.include Devise::Test::ControllerHelpers, type: :helper
config.include Devise::Test::ControllerHelpers, type: :view config.include Devise::Test::ControllerHelpers, type: :view
config.include ViewComponent::TestHelpers, type: :component
config.include ViewComponent::SystemTestHelpers, type: :component
end end
Shoulda::Matchers.configure do |config| Shoulda::Matchers.configure do |config|