votes: show votes when hovering over post score.
Make it so you can hover over a post's score to see the list of public upvotes. Also show the upvote count, the downvote count, and the upvote ratio.
This commit is contained in:
@@ -7,7 +7,9 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<span class="post-score"><%= post.score %></span>
|
||||
<span class="post-score">
|
||||
<%= link_to post.score, post_votes_path(search: { post_id: post.id }, variant: :compact) %>
|
||||
</span>
|
||||
|
||||
<% if can_vote? %>
|
||||
<% if downvoted? %>
|
||||
|
||||
@@ -6,5 +6,11 @@
|
||||
text-align: center;
|
||||
min-width: 1.25em;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
app/components/post_votes_tooltip_component.rb
Normal file
34
app/components/post_votes_tooltip_component.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This component represents the tooltip that displays when you hover over a post's score.
|
||||
class PostVotesTooltipComponent < ApplicationComponent
|
||||
attr_reader :post, :current_user
|
||||
delegate :upvote_icon, :downvote_icon, to: :helpers
|
||||
|
||||
def initialize(post:, current_user:)
|
||||
super
|
||||
@post = post
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def votes
|
||||
@post.votes.includes(:user).order(id: :desc)
|
||||
end
|
||||
|
||||
def vote_icon(vote)
|
||||
vote.is_positive? ? upvote_icon : downvote_icon
|
||||
end
|
||||
|
||||
def upvote_ratio
|
||||
return nil if votes.length == 0
|
||||
sprintf("(%.1f%%)", 100.0 * votes.select(&:is_positive?).length / votes.length)
|
||||
end
|
||||
|
||||
def voter_name(vote)
|
||||
if policy(vote).can_see_voter?
|
||||
link_to_user(vote.user, classes: "align-middle")
|
||||
else
|
||||
tag.i("hidden", class: "align-middle")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="post-votes-tooltip thin-scrollbar">
|
||||
<div class="text-center text-muted">
|
||||
+<%= post.up_score %> / <%= post.down_score %> <%= upvote_ratio %>
|
||||
</div>
|
||||
|
||||
<div class="post-voters">
|
||||
<% votes.each do |vote| %>
|
||||
<div class="post-voter truncate">
|
||||
<%= vote_icon(vote) %> <%= voter_name(vote) %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,65 @@
|
||||
import Utility from "../../javascript/src/javascripts/utility.js";
|
||||
import { delegate, hideAll } from 'tippy.js';
|
||||
import 'tippy.js/dist/tippy.css';
|
||||
|
||||
class PostVotesTooltipComponent {
|
||||
// Trigger on the post score link; see PostVotesComponent.
|
||||
static TARGET_SELECTOR = "span.post-votes span.post-score a";
|
||||
static SHOW_DELAY = 125;
|
||||
static HIDE_DELAY = 125;
|
||||
static DURATION = 250;
|
||||
static instance = null;
|
||||
|
||||
static initialize() {
|
||||
if ($(PostVotesTooltipComponent.TARGET_SELECTOR).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
PostVotesTooltipComponent.instance = delegate("body", {
|
||||
allowHTML: true,
|
||||
appendTo: document.querySelector("#post-votes-tooltips"),
|
||||
delay: [PostVotesTooltipComponent.SHOW_DELAY, PostVotesTooltipComponent.HIDE_DELAY],
|
||||
duration: PostVotesTooltipComponent.DURATION,
|
||||
interactive: true,
|
||||
maxWidth: "none",
|
||||
target: PostVotesTooltipComponent.TARGET_SELECTOR,
|
||||
theme: "common-tooltip",
|
||||
touch: false,
|
||||
|
||||
onShow: PostVotesTooltipComponent.onShow,
|
||||
onHide: PostVotesTooltipComponent.onHide,
|
||||
});
|
||||
}
|
||||
|
||||
static async onShow(instance) {
|
||||
let $target = $(instance.reference);
|
||||
let $tooltip = $(instance.popper);
|
||||
let postId = $target.parents("[data-id]").data("id");
|
||||
|
||||
hideAll({ exclude: instance });
|
||||
|
||||
try {
|
||||
$tooltip.addClass("tooltip-loading");
|
||||
|
||||
instance._request = $.get(`/post_votes?search[post_id]=${postId}`, { variant: "tooltip" });
|
||||
let html = await instance._request;
|
||||
instance.setContent(html);
|
||||
|
||||
$tooltip.removeClass("tooltip-loading");
|
||||
} catch (error) {
|
||||
if (error.status !== 0 && error.statusText !== "abort") {
|
||||
Utility.error(`Error displaying votes for post #${postId} (error: ${error.status} ${error.statusText})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async onHide(instance) {
|
||||
if (instance._request?.state() === "pending") {
|
||||
instance._request.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(PostVotesTooltipComponent.initialize);
|
||||
|
||||
export default PostVotesTooltipComponent;
|
||||
@@ -0,0 +1,16 @@
|
||||
.post-votes-tooltip {
|
||||
font-size: var(--text-xs);
|
||||
max-height: 240px;
|
||||
|
||||
.upvote-icon {
|
||||
color: var(--post-upvote-color);
|
||||
}
|
||||
|
||||
.downvote-icon {
|
||||
color: var(--post-downvote-color);
|
||||
}
|
||||
|
||||
.post-voter {
|
||||
max-width: 160px;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ class PostVotesController < ApplicationController
|
||||
def index
|
||||
@post_votes = authorize PostVote.visible(CurrentUser.user).paginated_search(params, count_pages: true)
|
||||
@post_votes = @post_votes.includes(:user, post: [:uploader, :media_asset]) if request.format.html?
|
||||
@post = Post.find(params.dig(:search, :post_id)) if params.dig(:search, :post_id).present?
|
||||
|
||||
respond_with(@post_votes)
|
||||
end
|
||||
|
||||
@@ -193,10 +193,10 @@ module ApplicationHelper
|
||||
to_sentence(links, **options)
|
||||
end
|
||||
|
||||
def link_to_user(user, text = nil)
|
||||
def link_to_user(user, text = nil, classes: nil, **options)
|
||||
return "anonymous" if user.blank?
|
||||
|
||||
user_class = "user user-#{user.level_string.downcase}"
|
||||
user_class = "user user-#{user.level_string.downcase} #{classes}"
|
||||
user_class += " user-post-approver" if user.can_approve_posts?
|
||||
user_class += " user-post-uploader" if user.can_upload_free?
|
||||
user_class += " user-banned" if user.is_banned?
|
||||
|
||||
@@ -20,6 +20,10 @@ module ComponentsHelper
|
||||
render PostVotesComponent.new(post: post, **options)
|
||||
end
|
||||
|
||||
def render_post_votes_tooltip(post, **options)
|
||||
render PostVotesTooltipComponent.new(post: post, **options)
|
||||
end
|
||||
|
||||
def render_post_navbar(post, **options)
|
||||
render PostNavbarComponent.new(post: post, **options)
|
||||
end
|
||||
|
||||
@@ -43,6 +43,7 @@ import PopupMenuComponent from "../../components/popup_menu_component/popup_menu
|
||||
import Post from "../src/javascripts/posts.js";
|
||||
import PostModeMenu from "../src/javascripts/post_mode_menu.js";
|
||||
import PostTooltip from "../src/javascripts/post_tooltips.js";
|
||||
import PostVotesTooltipComponent from "../../components/post_votes_tooltip_component/post_votes_tooltip_component.js";
|
||||
import RelatedTag from "../src/javascripts/related_tag.js";
|
||||
import Shortcuts from "../src/javascripts/shortcuts.js";
|
||||
import TagCounter from "../src/javascripts/tag_counter.js";
|
||||
@@ -64,6 +65,7 @@ Danbooru.PopupMenuComponent = PopupMenuComponent;
|
||||
Danbooru.Post = Post;
|
||||
Danbooru.PostModeMenu = PostModeMenu;
|
||||
Danbooru.PostTooltip = PostTooltip;
|
||||
Danbooru.PostVotesTooltipComponent = PostVotesTooltipComponent;
|
||||
Danbooru.RelatedTag = RelatedTag;
|
||||
Danbooru.Shortcuts = Shortcuts;
|
||||
Danbooru.TagCounter = TagCounter;
|
||||
|
||||
@@ -218,6 +218,9 @@ html {
|
||||
--post-artist-commentary-container-background: var(--grey-0);
|
||||
--post-artist-commentary-container-border-color: var(--grey-1);
|
||||
|
||||
--post-upvote-color: var(--link-color);
|
||||
--post-downvote-color: var(--red-5);
|
||||
|
||||
--note-body-background: #FFE;
|
||||
--note-body-text-color: var(--black);
|
||||
--note-body-border-color: var(--black);
|
||||
@@ -418,6 +421,9 @@ body[data-current-user-theme="dark"] {
|
||||
--post-artist-commentary-container-background: var(--grey-8);
|
||||
--post-artist-commentary-container-border-color: var(--grey-7);
|
||||
|
||||
--post-upvote-color: var(--link-color);
|
||||
--post-downvote-color: var(--red-4);
|
||||
|
||||
--unsaved-note-box-border-color: var(--red-5);
|
||||
--movable-note-box-border-color: var(--blue-5);
|
||||
--note-preview-border-color: var(--red-5);
|
||||
|
||||
@@ -22,6 +22,12 @@ $spacer: 0.25rem; /* 4px */
|
||||
.text-xs { font-size: var(--text-xs); }
|
||||
.text-sm { font-size: var(--text-sm); }
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.leading-none { line-height: 1; }
|
||||
|
||||
.absolute { position: absolute; }
|
||||
@@ -52,6 +58,9 @@ $spacer: 0.25rem; /* 4px */
|
||||
.p-0\.5 { padding: 0.5 * $spacer; }
|
||||
.p-4 { padding: 4 * $spacer; }
|
||||
|
||||
.pr-2 { padding-right: 2 * $spacer; }
|
||||
.pr-4 { padding-right: 4 * $spacer; }
|
||||
|
||||
.w-1\/4 { width: 25%; }
|
||||
.w-full { width: 100%; }
|
||||
|
||||
@@ -73,6 +82,60 @@ $spacer: 0.25rem; /* 4px */
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
|
||||
.thin-scrollbar {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-right: 2 * $spacer;
|
||||
overscroll-behavior: contain; // https://caniuse.com/css-overscroll-behavior
|
||||
|
||||
// Firefox only
|
||||
// https://caniuse.com/?search=scrollbar-width
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--post-tooltip-scrollbar-background);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--post-tooltip-scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:active {
|
||||
background: var(--post-tooltip-scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--post-tooltip-scrollbar-track-background);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track:hover {
|
||||
background: var(--post-tooltip-scrollbar-track-background);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track:active {
|
||||
background: var(--post-tooltip-scrollbar-track-background);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 660px) {
|
||||
.md\:inline-block { display: inline-block; }
|
||||
.md\:flex { display: flex; }
|
||||
|
||||
@@ -1,50 +1,6 @@
|
||||
$tooltip-line-height: 16px;
|
||||
$tooltip-body-height: $tooltip-line-height * 4; // 4 lines high.
|
||||
|
||||
@mixin thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-button {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--post-tooltip-scrollbar-background);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--post-tooltip-scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:active {
|
||||
background: var(--post-tooltip-scrollbar-thumb-color);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--post-tooltip-scrollbar-track-background);
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track:hover {
|
||||
background: var(--post-tooltip-scrollbar-track-background);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track:active {
|
||||
background: var(--post-tooltip-scrollbar-track-background);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="post-tooltip"] {
|
||||
min-width: 20em;
|
||||
max-width: 40em !important;
|
||||
@@ -59,7 +15,6 @@ $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high.
|
||||
}
|
||||
|
||||
.post-tooltip-body {
|
||||
@include thin-scrollbar;
|
||||
max-height: $tooltip-body-height;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
<div id="tooltips">
|
||||
<div id="post-tooltips"></div>
|
||||
<div id="user-tooltips"></div>
|
||||
<div id="post-votes-tooltips"></div>
|
||||
<div id="popup-menus"></div>
|
||||
</div>
|
||||
|
||||
|
||||
7
app/views/post_votes/_search.html.erb
Normal file
7
app/views/post_votes/_search.html.erb
Normal file
@@ -0,0 +1,7 @@
|
||||
<%= search_form_for(post_votes_path) do |f| %>
|
||||
<%= f.input :user_name, label: "Voter", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %>
|
||||
<%= f.input :post_id, label: "Post", input_html: { value: params[:search][:post_id] } %>
|
||||
<%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %>
|
||||
<%= f.input :score, collection: [["+3", "3"], ["+1", "1"], ["-1", "-1"], ["-3", "-3"]], include_blank: true, selected: params[:search][:score] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
24
app/views/post_votes/index.html+compact.erb
Normal file
24
app/views/post_votes/index.html+compact.erb
Normal file
@@ -0,0 +1,24 @@
|
||||
<%= render "posts/partials/common/secondary_links" %>
|
||||
|
||||
<div id="c-post-votes">
|
||||
<div id="a-index">
|
||||
<%= render "search" %>
|
||||
|
||||
<%= table_for @post_votes, class: "striped autofit" do |t| %>
|
||||
<% t.column "Score" do |vote| %>
|
||||
<%= link_to sprintf("%+d", vote.score), post_votes_path(search: { score: vote.score }) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Voter" do |vote| %>
|
||||
<%= link_to_user vote.user %>
|
||||
<%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %>
|
||||
<% end %>
|
||||
|
||||
<% t.column "Created" do |vote| %>
|
||||
<%= time_ago_in_words_tagged(vote.created_at) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<%= numbered_paginator(@post_votes) %>
|
||||
</div>
|
||||
</div>
|
||||
1
app/views/post_votes/index.html+tooltip.erb
Normal file
1
app/views/post_votes/index.html+tooltip.erb
Normal file
@@ -0,0 +1 @@
|
||||
<%= render_post_votes_tooltip(@post, current_user: CurrentUser.user) %>
|
||||
@@ -1,12 +1,6 @@
|
||||
<div id="c-post-votes">
|
||||
<div id="a-index">
|
||||
<%= search_form_for(post_votes_path) do |f| %>
|
||||
<%= f.input :user_name, label: "Voter", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %>
|
||||
<%= f.input :post_id, label: "Post", input_html: { value: params[:search][:post_id] } %>
|
||||
<%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %>
|
||||
<%= f.input :score, collection: [["+3", "3"], ["+1", "1"], ["-1", "-1"], ["-3", "-3"]], include_blank: true, selected: params[:search][:score] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
<%= render "search" %>
|
||||
|
||||
<%= table_for @post_votes, class: "striped autofit" do |t| %>
|
||||
<% t.column "Post" do |vote| %>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="post-tooltip-body <%= "has-preview" if params[:preview].truthy? %>">
|
||||
<div class="post-tooltip-body thin-scrollbar <%= "has-preview" if params[:preview].truthy? %>">
|
||||
<div class="post-tooltip-body-left">
|
||||
<% if params[:preview].truthy? %>
|
||||
<%= post_preview(@post, show_deleted: true, compact: true) %>
|
||||
|
||||
@@ -10,8 +10,8 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
|
||||
context "index action" do
|
||||
setup do
|
||||
@user = create(:user, enable_private_favorites: true)
|
||||
create(:post_vote, user: @user, score: 1)
|
||||
create(:post_vote, user: @user, score: -1)
|
||||
@upvote = create(:post_vote, user: @user, score: 1)
|
||||
@downvote = create(:post_vote, user: @user, score: -1)
|
||||
end
|
||||
|
||||
should "render" do
|
||||
@@ -19,6 +19,16 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
should "render for a compact view" do
|
||||
get post_votes_path(variant: "compact")
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
should "render for a tooltip" do
|
||||
get post_votes_path(search: { post_id: @upvote.post_id }, variant: "tooltip")
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
context "as a user" do
|
||||
should "show the user all their own votes" do
|
||||
get_auth post_votes_path, @user
|
||||
|
||||
Reference in New Issue
Block a user