[Users] Rework the profile page (#931)

This commit is contained in:
Cinder 2025-02-26 05:34:43 -08:00 committed by GitHub
parent e304030e8a
commit 0dd17474aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 1016 additions and 325 deletions

View File

@ -121,6 +121,10 @@ module ApplicationHelper
time_tag(time.strftime("%Y-%m-%d %H:%M"), time)
end
def compact_date(time)
time_tag(time.strftime("%Y-%m-%d"), time)
end
def external_link_to(url, truncate: nil, strip_scheme: false, link_options: {})
text = url
text = text.gsub(%r!\Ahttps?://!i, "") if strip_scheme
@ -189,21 +193,6 @@ module ApplicationHelper
end
end
def simple_avatar(user, **options)
return "" if user.nil?
post_id = user.avatar_id
deferred_post_ids.add(post_id) if post_id
klass = options.delete(:class)
named = options.delete(:named)
tag.a href: user_path(user), class: "simple-avatar placeholder #{klass}", data: { id: post_id, name: user.name } do
tag.span(class: "simple-avatar-button") do
concat tag.span(user.pretty_name, class: "simple-avatar-name") if named
concat tag.span(class: "simple-avatar-image", data: { name: user.name[0].capitalize })
end
end
end
def unread_dmails(user)
if user.has_mail?
"(#{user.unread_dmail_count})"

View File

@ -20,8 +20,18 @@ module IconHelper
user: %(<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>),
# Utility
plus: %(<path d="M5 12h14"/><path d="M12 5v14"/>),
times: %(<path d="M18 6 6 18"/><path d="m6 6 12 12"/>),
reset: %(<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>),
replace: %(<path d="M14 4a2 2 0 0 1 2-2"/><path d="M16 10a2 2 0 0 1-2-2"/><path d="M20 2a2 2 0 0 1 2 2"/><path d="M22 8a2 2 0 0 1-2 2"/><path d="m3 7 3 3 3-3"/><path d="M6 10V5a3 3 0 0 1 3-3h1"/><rect x="2" y="14" width="8" height="8" rx="2"/>),
upload: %(<path d="M12 13v8"/><path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="m8 17 4-4 4 4"/>),
stamp: %(<path d="M19.27 13.73A2.5 2.5 0 0 0 17.5 13h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-1.5c0-.66-.26-1.3-.73-1.77Z"/><path d="M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-3-3c-1.66 0-3 1-3 3s1 2 1 3.5V13"/>),
power: %(<path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/>),
circle_help: %(<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>),
notepad: %(<path d="M8 2v4"/><path d="M12 2v4"/><path d="M16 2v4"/><path d="M16 4h2a2 2 0 0 1 2 2v2"/><path d="M20 12v2"/><path d="M20 18v2a2 2 0 0 1-2 2h-1"/><path d="M13 22h-2"/><path d="M7 22H6a2 2 0 0 1-2-2v-2"/><path d="M4 14v-2"/><path d="M4 8V6a2 2 0 0 1 2-2h2"/><path d="M8 10h6"/><path d="M8 14h8"/><path d="M8 18h5"/>),
flag_left: %(<path d="M17 22V2L7 7l10 5"/>),
ticket: %(<path d="M2 9a3 3 0 0 1 0 6v2a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2a3 3 0 0 1 0-6V7a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2Z"/><path d="M13 5v2"/><path d="M13 17v2"/><path d="M13 11v2"/>),
key_square: %(<path d="M12.4 2.7a2.5 2.5 0 0 1 3.4 0l5.5 5.5a2.5 2.5 0 0 1 0 3.4l-3.7 3.7a2.5 2.5 0 0 1-3.4 0L8.7 9.8a2.5 2.5 0 0 1 0-3.4z"/><path d="m14 7 3 3"/><path d="m9.4 10.6-6.814 6.814A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814"/>),
# Pagination
chevron_left: %(<path d="m15 18-6-6 6-6"/>),

View File

@ -11,4 +11,47 @@ module UsersHelper
domain = email.split("@").last
link_to "»", users_path(search: { email_matches: "*@#{domain}" })
end
def simple_avatar(user, **options)
return "" if user.nil?
post_id = user.avatar_id
deferred_post_ids.add(post_id) if post_id
klass = options.delete(:class)
named = options.delete(:named)
tag.a href: user_path(user), class: "simple-avatar placeholder #{klass}", data: { id: post_id, name: user.name } do
tag.span(class: "avatar-button") do
concat tag.span(user.pretty_name, class: "avatar-name") if named
concat tag.span(class: "avatar-image", data: { name: user.name[0].capitalize })
end
end
end
def profile_avatar(user, **options)
return if user.nil?
post_id = user.avatar_id
deferred_post_ids.add(post_id) if post_id
klass = options.delete(:class)
render "/application/profile_avatar", user: user, post_id: post_id, klass: klass
end
def user_level_badge(user)
return if user.nil?
tag.span(class: "level-badge level-#{user.level_string.downcase}") do
user.level_string.upcase
end
end
def user_feedback_badge(user)
return if user.nil?
feedbacks = user.feedback_pieces
deleted = CurrentUser.user.is_staff? ? feedbacks[:deleted] : 0
active = feedbacks[:positive] + feedbacks[:neutral] + feedbacks[:negative]
render "/application/feedback_badge", user: user, positive: feedbacks[:positive], neutral: feedbacks[:neutral], negative: feedbacks[:negative], deleted: deleted, active: active
end
end

View File

@ -9,7 +9,7 @@ Thumbnails.initialize = function () {
const replacedPosts = [];
// Avatar special case
for (const post of $(".simple-avatar.placeholder")) {
for (const post of $(".simple-avatar.placeholder, .profile-avatar.placeholder")) {
const $post = $(post);
$post.removeClass("placeholder");
@ -21,7 +21,11 @@ Thumbnails.initialize = function () {
$("<img>")
.attr("src", postData["preview_url"])
.appendTo($post.find("span.simple-avatar-image"));
.appendTo($post.find("span.avatar-image"));
if ($post.hasClass("profile-avatar"))
$post.attr("href", "/posts/" + postID);
continue;
}

View File

@ -0,0 +1,25 @@
import LStorage from "./utility/storage";
const Users = {};
Users.init_section = function ($wrapper) {
const $header = $wrapper.find(".profile-section-header").first();
const $body = $(".profile-section-body").first();
const name = $wrapper.attr("name");
if (!name || !$header.length || !$body.length) return;
let state = LStorage.Users[name];
if (state) $wrapper.removeClass("hidden");
$header.on("click", () => {
$wrapper.toggleClass("hidden", state);
state = !state;
LStorage.Users[name] = state;
});
};
$(() => {
for (const one of $((".profile-section")))
Users.init_section($((one)));
});

View File

@ -190,6 +190,17 @@ LStorage.Blacklist = {
StorageUtils.bootstrapSome(LStorage.Blacklist, ["Collapsed"]);
// Users page config
LStorage.Users = {
/** @returns {boolean} True to show staff stats, false to hide them */
StaffStats: ["e6.users.staffstats", false],
/** @returns {boolean} True to show user stats, false to hide them */
StaffNotes: ["e6.users.staffnotes", false],
};
StorageUtils.bootstrapMany(LStorage.Users);
/**
* Patches the add, delete, and clear methods for the filter cache set.
* Otherwise, modifying the set with these methods would not update the local storage

View File

@ -43,6 +43,7 @@
@import "common/user_styles.scss";
@import "common/voting.scss";
@import "views/application/application";
@import "views/posts/posts";
@import "views/users/users";

View File

@ -17,5 +17,5 @@ $st-values: (
@function padding($value) { @return st-value(($value)); }
@mixin st-padding($value) { padding: padding(($value)); }
@function radius($value) { @return st-value($value); }
@mixin st-radius($value) { border-radius: radius($value); }
@function radius($value: 025) { @return st-value($value); }
@mixin st-radius($value: 025) { border-radius: radius($value); }

View File

@ -76,11 +76,11 @@ nav.navigation {
}
a.simple-avatar {
.simple-avatar-button {
.avatar-button {
padding: 0;
gap: 0;
.simple-avatar-name {
.avatar-name {
padding: 0.5rem;
@include window-smaller-than(32rem) {
@ -88,7 +88,7 @@ nav.navigation {
}
}
.simple-avatar-image {
.avatar-image {
display: flex;
justify-content: center;
align-items: center;
@ -119,7 +119,7 @@ nav.navigation {
}
@include window-smaller-than(32rem) {
&.sign-in .simple-avatar-image {
&.sign-in .avatar-image {
background: themed("color-foreground");
}
}
@ -495,18 +495,18 @@ nav.navigation, html.nav-toggled nav.navigation {
padding: 0;
height: 100%;
.simple-avatar-button {
.avatar-button {
background: none;
color: inherit;
align-items: start;
font-size: 0.875rem;
line-height: 0.875rem;
.simple-avatar-name {
.avatar-name {
padding: 0 0.5rem;
}
.simple-avatar-image {
.avatar-image {
height: 3rem;
width: 3rem;
background: themed("color-foreground");
@ -528,9 +528,9 @@ nav.navigation, html.nav-toggled nav.navigation {
}
// Stage 2: account label
.collapse-2 .simple-avatar-name { display: none; }
.collapse-2 .avatar-name { display: none; }
@include window-larger-than(65rem) {
.collapse-2 .simple-avatar-name { display: unset; }
.collapse-2 .avatar-name { display: unset; }
}
}
}
@ -564,10 +564,10 @@ body.c-static.a-home {
position: static;
height: unset;
padding: 0;
.simple-avatar-button {
.avatar-button {
height: unset;
align-items: center;
.simple-avatar-image { display: none; }
.avatar-image { display: none; }
}
}
}

View File

@ -15,7 +15,7 @@ div#c-users {
gap: 1em;
& > div {
max-width: 1600px;
max-width: 100rem;
box-sizing: border-box;
}
@ -76,107 +76,7 @@ div#c-users {
// Middle section: uploads and favorites
.blacklist-ui { padding: 0; }
.posts-section {
display: flex;
flex-flow: column;
gap: 1em;
.profile-sample {
display: grid;
grid-template: "p-header"
"p-links"
"p-posts";
gap: 0.5em 0;
@include window-larger-than(800px) {
grid-template: "p-header p-links"
"p-posts p-posts";
grid-template-columns: 12em 1fr;
gap: 0 0.5em;
}
}
.profile-sample-header a, .profile-sample-links a {
display: block;
box-sizing: border-box;
align-content: center;
text-align: center;
height: 100%;
padding: 0.5em;
border-radius: 6px;
background-color: themed("color-section");
&:hover {
background-color: themed("color-section-lighten-5");
}
&:focus, &:active {
outline: 0;
color: themed("color-link-active");
}
}
.profile-sample-header {
grid-area: p-header;
display: flex;
a {
font-size: 1.25em;
font-weight: bold;
width: 100%;
}
@include window-larger-than(800px) {
a { border-radius: 6px 6px 0 0; }
}
}
.profile-sample-links {
grid-area: p-links;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
text-align: center;
span {
padding: 0.5em;
color: themed("color-text-muted");
}
.spacer { display: none; }
.profile-comments-link {
grid-row: 1 / 3;
grid-column: 3;
}
@include window-larger-than(800px) {
display: flex;
a { height: min-content; }
.spacer { display: block; }
}
}
.profile-sample-posts {
grid-area: p-posts;
display: flex;
overflow: auto;
justify-content: center;
flex-wrap: wrap;
gap: 1em;
padding: 0.5em;
background: var(--color-section);
border-radius: 6px;
// Desktop
@include window-larger-than(800px) {
flex-wrap: nowrap;
border-top-left-radius: 0;
justify-content: flex-start;
}
}
}
// Bottom section: about me and commission info

View File

@ -0,0 +1,3 @@
@import "avatar";
@import "feedback_badge";
@import "level_badge";

View File

@ -0,0 +1,41 @@
.profile-avatar {
.avatar-image {
display: flex;
width: 5rem;
height: 5rem;
position: relative;
background: themed("color-section");
@include st-radius;
// Letter if no avatar image
&::after {
content: attr(data-initial);
display: flex;
justify-content: center;
align-items: center;
font-size: 5rem;
font-weight: bold;
color: themed("color-foreground");
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
// On top of the letter
img {
width: 100%;
height: 100%;
object-fit: cover;
z-index: 1;
@include st-radius;
}
}
}

View File

@ -0,0 +1,103 @@
$hex-size: 1.25rem;
.user-record {
display: flex;
height: $hex-size * 0.6; // 0.75rem
width: $hex-size; // 1.25rem
margin: ($hex-size / 5) 0; // 0.25rem 0
justify-content: center;
align-items: center;
font-size: ($hex-size / 5 * 4) * 0.9;
svg { width: ($hex-size / 5 * 4) * 0.9; }
background: palette("plain-black");
color: white;
// Place corners under the text
position: relative;
z-index: 1;
&::before, &::after {
content: "";
position: absolute;
right: 0;
left: 0;
border-left: ($hex-size / 2) solid transparent;
border-right: ($hex-size / 2) solid transparent;
z-index: -1;
}
&::before {
top: -($hex-size / 5);
border-bottom: ($hex-size / 5) solid palette("plain-black");
}
&::after {
bottom: -($hex-size / 5);
border-top: ($hex-size / 5) solid palette("plain-black");
}
// Variations
&.deleted {
background: palette("background-yellow");
&::before { border-bottom-color: palette("background-yellow"); }
&::after { border-top-color: palette("background-yellow"); }
}
&.negative {
background: palette("background-red");
&::before { border-bottom-color: palette("background-red"); }
&::after { border-top-color: palette("background-red"); }
}
&.neutral {
background: palette("background-grey");
&::before { border-bottom-color: palette("background-grey"); }
&::after { border-top-color: palette("background-grey"); }
}
&.positive {
background: palette("background-green");
&::before { border-bottom-color: palette("background-green"); }
&::after { border-top-color: palette("background-green"); }
}
}
.user-records-list {
display: flex;
flex-flow: row;
gap: 0.25rem;
&:hover .user-record {
&.deleted {
background: palette("background-yellow-d5");
&::before { border-bottom-color: palette("background-yellow-d5"); }
&::after { border-top-color: palette("background-yellow-d5"); }
}
&.negative {
background: palette("background-red-d5");
&::before { border-bottom-color: palette("background-red-d5"); }
&::after { border-top-color: palette("background-red-d5"); }
}
&.neutral {
background: palette("background-grey-d5");
&::before { border-bottom-color: palette("background-grey-d5"); }
&::after { border-top-color: palette("background-grey-d5"); }
}
&.positive {
background: palette("background-green-d5");
&::before { border-bottom-color: palette("background-green-d5"); }
&::after { border-top-color: palette("background-green-d5"); }
}
}
}

View File

@ -0,0 +1,40 @@
$user-levels: (
"unactivated"
"blocked"
"member"
"privileged"
"former-staff"
"janitor"
"moderator"
"admin"
);
$user-level-text: (
"moderator": #ffffff,
"janitor": #ffffff,
);
.level-badge {
font-weight: bold;
font-size: 0.65rem;
color: black;
background: themed("color-user-member");
padding: 0.1rem 0.25rem;
@include st-radius;
@each $level in $user-levels {
&.level-#{$level} {
background: themed("color-user-" + $level);
}
}
@each $level, $color in $user-level-text {
&.level-#{$level} {
color: $color;
}
}
&.level-unactivated {
background: themed("palette-background-red");
color: #ffffff;
}
}

View File

@ -1 +1,2 @@
@import "show/show";
@import "edit/edit";

View File

@ -106,8 +106,9 @@ tabs-content {
}
tab-entry {
display: none;
&.active { display: grid; }
display: none !important;
&.active { display: grid !important; }
&.flex.active { display: flex !important; }
grid-template-areas: "head" "body" "hint";
grid-template-columns: 1fr;

View File

@ -0,0 +1,10 @@
body.c-users.a-show {
@import "partials/about";
@import "partials/ban_banner";
@import "partials/card";
@import "partials/post_summary";
@import "partials/profile_section";
@import "partials/staff_info";
@import "partials/user_info";
}

View File

@ -0,0 +1,44 @@
tabs-menu, tabs-content {
max-width: 100rem;
}
#profile-tabs[data-has-about="false"][data-has-artinfo="false"] {
display: none;
}
.profile-about-section, .profile-artinfo-section {
flex-flow: column;
align-items: start;
tab-head {
font-weight: bold;
font-size: 1rem;
}
}
@include window-larger-than(50rem) {
tabs-menu { display: none; }
tabs-content {
display: grid;
grid-template-columns: 15rem 1fr;
grid-template-rows: min-content min-content;
gap: 1rem;
}
.profile-user-info {
display: grid !important;
grid-template-areas: unset;
grid-row: 1 / 4;
height: min-content;
}
.profile-about-section {
display: flex !important;
grid-column: 2 / -1;
}
.profile-artinfo-section {
display: flex !important;
grid-column: 2 / -1;
}
}

View File

@ -0,0 +1,9 @@
.profile-ban {
background: var(--palette-background-red);
padding: 0.5rem;
@include st-radius;
.styled-dtext p:last-child {
margin-bottom: 0;
}
}

View File

@ -0,0 +1,87 @@
.profile-card {
display: grid;
grid-template-columns: min-content auto;
gap: 1rem;
// Info block
.profile-info {
display: flex;
flex-flow: column;
justify-content: center;
gap: 0.25rem;
}
.profile-name {
display: flex;
flex-flow: row;
gap: 0.5rem;
align-items: center;
a {
color: themed("color-text");
font-size: 1.25rem;
}
}
.profile-joined {
font-size: 0.75rem;
color: themed("color-text-muted");
margin-top: -0.25rem;
}
}
.profile-quickie {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
.entry {
display: flex;
flex-flow: column;
align-items: center;
}
.entry > a {
display: grid;
grid-template-columns: min-content 1fr;
grid-template-areas:
"icon header"
"icon title ";
column-gap: 0.25rem;
align-items: center;
width: 7rem;
padding: 0.5rem;
font-size: 1rem;
@include st-radius;
background: themed("color-section");
&:hover { background: themed("color-section-lighten-5"); }
.entry-icon {
grid-area: icon;
height: 1.5rem;
width: 1.5rem;
color: themed("color-link-active");
}
.entry-header {
grid-area: header;
font-size: 1.1rem;
line-height: 1.1rem;
white-space: nowrap;
}
.entry-title {
grid-area: title;
font-size: 0.9rem;
line-height: 0.9rem;
white-space: nowrap;
}
}
.entry-extra {
a, a:visited { color: themed("color-text-muted"); }
a:hover, a:active { color: themed("color-link-active"); }
}
}

View File

@ -0,0 +1,107 @@
.posts-section {
display: flex;
flex-flow: column;
gap: 1em;
// Hack to fix spacing on mobile
margin-top: 1rem;
@include window-larger-than(50rem) {
margin-top: 0;
}
.profile-sample {
display: grid;
grid-template: "p-header"
"p-links"
"p-posts";
gap: 0.5em 0;
@include window-larger-than(50rem) {
grid-template: "p-header p-links"
"p-posts p-posts";
grid-template-columns: 12em 1fr;
gap: 0 0.5em;
}
}
.profile-sample-header a, .profile-sample-links a {
display: block;
box-sizing: border-box;
align-content: center;
text-align: center;
height: 100%;
padding: 0.5em;
border-radius: 6px;
background-color: themed("color-section");
&:hover {
background-color: themed("color-section-lighten-5");
}
&:focus, &:active {
outline: 0;
color: themed("color-link-active");
}
}
.profile-sample-header {
grid-area: p-header;
display: flex;
a {
font-size: 1.25em;
font-weight: bold;
width: 100%;
}
@include window-larger-than(800px) {
a { border-radius: 6px 6px 0 0; }
}
}
.profile-sample-links {
grid-area: p-links;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
text-align: center;
span {
padding: 0.5em;
color: themed("color-text-muted");
}
.spacer { display: none; }
.profile-comments-link {
grid-row: 1 / 3;
grid-column: 3;
}
@include window-larger-than(50rem) {
display: flex;
a { height: min-content; }
.spacer { display: block; }
}
}
.profile-sample-posts {
grid-area: p-posts;
display: flex;
overflow: auto;
justify-content: center;
flex-wrap: wrap;
gap: 1em;
padding: 0.5em;
background: var(--color-section);
border-radius: 6px;
// Desktop
@include window-larger-than(50rem) {
flex-wrap: nowrap;
border-top-left-radius: 0;
justify-content: flex-start;
}
}
}

View File

@ -0,0 +1,35 @@
.profile-section {
background: themed("color-section");
.profile-section-header {
background: themed("color-section-lighten-5");
@include st-radius;
padding: 0.25rem 0.5rem;
cursor: pointer;
&::before {
content: "";
display: inline-flex;
transition: transform 200ms;
margin-right: 0.25rem;
}
}
.profile-section-body {
padding: 0.25rem 0.5rem;
border-radius: 0 0 radius() radius();
overflow: hidden;
}
// Collapsed
&.hidden {
.profile-section-header::before {
transform: rotate(-90deg);
}
.profile-section-body {
padding: 0 0.5rem;
height: 0;
}
}
}

View File

@ -0,0 +1,16 @@
.profile-staff-info {
display: grid;
grid-template-columns: min-content 1fr;
gap: 0.25rem 1rem;
h4 {
white-space: nowrap;
}
.block { grid-column: 1 / -1; }
h4.block { margin-bottom: -0.25rem; }
@include window-larger-than(50rem) {
grid-template-columns: min-content 1fr min-content 1fr;
}
}

View File

@ -0,0 +1,71 @@
.profile-user-info {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(auto-fit, minmax(min-content, 5rem));
background: themed("color-section");
@include st-radius;
@include window-larger-than(38rem) { grid-template-columns: 1fr 1fr; }
@include window-larger-than(50rem) { grid-template-columns: 1fr; }
.profile-line {
display: grid;
grid-template-columns: 1fr min-content;
padding: 1rem 0.5rem;
border-bottom: 2px solid themed("color-foreground");
h4 {
// Not necessary, but it aligns
// with the -extra line better
display: flex;
align-items: center;
gap: 0.25em;
svg {
width: 1em;
height: 1em;
vertical-align: middle;
}
}
&-number {
white-space: nowrap;
}
&-extra {
grid-column: 1 / -1;
font-size: 90%;
color: themed("color-text-muted");
margin-left: 1.25em;
}
// Stats display section
&-show {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 2rem;
margin-top: 0.5rem;
.entry {
text-align: center;
width: 5rem;
& > span { color: themed("color-text-muted"); }
}
}
// Dot-separated, less important
&-list {
display: flex;
gap: 0.25em;
a:not(:last-child)::after {
content: "";
margin-left: 0.25rem;
color: themed("color-text-muted");
}
}
&:last-child { border-bottom: none; }
}
}

View File

@ -574,6 +574,11 @@ class User < ApplicationRecord
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4) - pieces[:pending]
end
def upload_limit_max
pieces = upload_limit_pieces
base_upload_limit + (pieces[:approved] / 10) - (pieces[:deleted] / 4)
end
def upload_limit_pieces
@upload_limit_pieces ||= begin
deleted_count = Post.deleted.for_user(id).count
@ -739,6 +744,28 @@ class User < ApplicationRecord
user_status.ticket_count
end
def feedback_pieces
@feedback_pieces ||= begin
count = {
deleted: 0,
negative: 0,
neutral: 0,
positive: 0,
}
feedback.each do |one|
if one.is_deleted
count[:deleted] += 1
next
end
count[one.category.to_sym] += 1
end
count
end
end
def positive_feedback_count
feedback.active.positive.count
end

View File

@ -53,6 +53,11 @@ class UserPresenter
= <abbr title="User Upload Limit Remaining">#{user.upload_limit}</abbr>}.html_safe
end
def upload_limit_short
return "none" if user.can_upload_free?
"#{user.upload_limit} / #{user.upload_limit_max}"
end
def uploads
posts = Post.tag_match("user:#{user.name}").limit(8)
PostsDecorator.decorate_collection(posts)

View File

@ -0,0 +1,34 @@
<a
href="<%= user_feedbacks_path(search: { user_id: user.id }) %>"
class="user-records-list"
data-negative="<%= negative %>"
data-neutral="<%= negative %>"
data-positive="<%= neutral %>"
data-total="<%= positive - negative %>"
>
<% if deleted > 0 %>
<span class="user-record deleted"><%= deleted %></span>
<% end %>
<% if negative > 0 %>
<span class="user-record negative"><%= negative %></span>
<% end %>
<% if neutral > 0 %>
<span class="user-record neutral"><%= neutral %></span>
<% end %>
<% if positive > 0 %>
<span class="user-record positive"><%= positive %></span>
<% end %>
</a>
<% if CurrentUser.is_moderator? && CurrentUser.user != user && active == 0 %>
<a
href="<%= new_user_feedback_path(user_feedback: { user_id: user.id, category: "neutral" }) %>"
class="user-records-list"
title="New Feedback"
>
<span class="user-record neutral"><%= svg_icon(:plus) %></span>
</a>
<% end %>

View File

@ -0,0 +1,11 @@
<a
href="<%= user_path(user) %>"
class="profile-avatar placeholder<%= klass.nil? ? "" : klass%>"
data-id="<%= post_id %>"
data-name="<%= user.name %>"
>
<span
class="avatar-image"
data-initial="<%= user.name[0].capitalize%>"
></span>
</a>

View File

@ -20,11 +20,11 @@
</a>
<% if CurrentUser.is_anonymous? %>
<a href="<%= new_session_path %>" class="simple-avatar nav-controls-profile collapse-2">
<span class="simple-avatar-button sign-in">
<span class="simple-avatar-name">
<span class="avatar-button sign-in">
<span class="avatar-name">
Sign In
</span>
<span class="simple-avatar-image">
<span class="avatar-image">
<%= svg_icon(:log_in) %>
</span>
</span>

View File

@ -1,15 +1,12 @@
<% if CurrentUser.can_view_staff_notes? %>
<div class="staff-notes-section styled-dtext">
<details>
<summary>Staff Notes (<%= user.staff_notes.count %>)</summary>
<div>
<h4><%= link_to "Staff Notes", staff_notes_path(search: { user_id: user.id }) %></h4>
<div class="profile-section hidden" name="StaffNotes">
<div class="profile-section-header">Staff Notes (<%= user.staff_notes.count %>)</div>
<div class="profile-section-body styled-dtext">
<%= render "staff_notes/partials/list_of_notes", staff_notes: user.staff_notes.limit(15), show_receiver_name: false %>
<div class="new-staff-note">
<p><%= link_to "Create »", new_staff_note_path(search: { user_id: user.id }), class: "expand-new-staff-note" %></p>
<%= render "staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id), hidden: true %>
</div>
</div>
</details>
</div>
<% end %>

View File

@ -1,14 +0,0 @@
<div class="about-section">
<% if user.profile_about.present? %>
<div class="profile-about-entry" id="about-info">
<h3>About</h3>
<div class="content dtext-container"><%= format_text(user.profile_about, allow_color: true) %></div>
</div>
<% end %>
<% if user.profile_artinfo.present? %>
<div class="profile-about-entry" id="about-artinfo">
<h3>Artist Information</h3>
<div class="content dtext-container"><%= format_text(user.profile_artinfo, allow_color: true) %></div>
</div>
<% end %>
</div>

View File

@ -1,156 +0,0 @@
<div class="stats-section">
<div class="profile-avatar">
<%= user_avatar @user %>
</div>
<div class="profile-stats">
<h1><%= link_to_user @user %></h1>
<div class="user-statistics">
<div class="column">
<span>Join Date</span>
<span><%= compact_time @user.created_at %></span>
<span>Level</span>
<span><%= "(Unactivated)" unless user.is_verified? %> <%= presenter.level %></span>
<% if user.is_banned? && user.recent_ban %>
<span>Ban reason</span>
<span class="dtext-container"><%= format_text presenter.ban_reason %></span>
<% end %>
<span>Posts</span>
<span>
<%= presenter.active_upload_count(self) %>
[<%= link_to "pending", posts_path(tags: "user:#{user.name} status:pending") %>]
(<%= link_to "comments on", comments_path(group_by: :comment, search: {poster_id: user.id}) %>)
<% if CurrentUser.is_moderator? %>
(<%= link_to "votes", action: "index", controller: "post_votes", search: { user_name: user.name } %>)
<% end %>
</span>
<span>Deleted</span>
<span>
<%= presenter.deleted_upload_count(self) %>
</span>
<span>Replaced</span>
<span>
<%= presenter.replaced_upload_count(self) %>
[<%= link_to "pending", post_replacements_path(search: { creator_name: user.name }) %>]
</span>
<span>Rejected</span>
<span><%= presenter.rejected_replacements_count(self) %></span>
<span>Favorites</span>
<span>
<%= presenter.favorite_count(self) %>
</span>
<span>Forum Posts</span>
<span>
<%= presenter.forum_post_count(self) %>
(<%= link_to "mentions", forum_posts_path(search: { body_matches: user.name }) %>)
</span>
<span>Comments</span>
<span>
<%= presenter.comment_count(self) %> on <%= presenter.commented_posts_count(self) %> posts
(<%= link_to "mentions", comments_path(group_by: :comment, search:{ body_matches: user.name }) %>)
<% if CurrentUser.is_moderator? %>
(<%= link_to "votes", action: "index", controller: "comment_votes", search: { user_name: user.name } %>)
<% end %>
</span>
<% if user.can_approve_posts? || Post.where(approver: user).exists? %>
<span>Approvals</span>
<span><%= presenter.approval_count(self) %></span>
<% end %>
<% if CurrentUser.user.id == user.id || CurrentUser.is_janitor? %>
<% if presenter.previous_names(self).present? %>
<span>Previous Names</span>
<span><%= presenter.previous_names(self) %> -> <%= user.name %></span>
<% end %>
<% end %>
<% if CurrentUser.is_admin? %>
<span>Email</span>
<span>
<%= user.email %>
<%= email_domain_search(user.email) %>
</span>
<span>Last IP</span>
<span><%= link_to_ip(user.last_ip_addr) %></span>
<% end %>
</div>
<div class="column">
<span>Feedback</span>
<span>
<%= presenter.feedbacks %>
<%= link_to("List", user_feedbacks_path(search: { user_id: @user.id })) %>
<% if CurrentUser.is_moderator? && @user.feedback.active.count == 0 %>
| <%= link_to("Create", new_user_feedback_path(user_feedback: { user_id: @user.id, category: "neutral" })) %>
<% end %>
</span>
<span>Permissions</span>
<span><%= presenter.permissions %></span>
<span>Upload Limit</span>
<span>
<%= presenter.upload_limit(self) %>
<% if CurrentUser.user.id == user.id %>
(<%= link_to "help", upload_limit_users_path %>)
<% else %>
(<%= link_to "help", wiki_page_path(id: "upload_limit") %>)
<% end %>
</span>
<span>Post Changes</span>
<span>
<%= presenter.post_version_count(self) %>
<% if CurrentUser.is_moderator? && UserRevert.can_revert?(user) %>
[<%= link_to "revert all", new_user_revert_path(user_id: user.id) %>]
<% end %>
</span>
<span>Wiki Changes</span>
<span><%= presenter.wiki_page_version_count(self) %></span>
<span>Note Changes</span>
<span><%= presenter.note_version_count(self) %> on <%= presenter.noted_posts_count(self) %> posts</span>
<span>Artist Changes</span>
<span><%= presenter.artist_version_count(self) %></span>
<span>Pool Changes</span>
<span><%= presenter.pool_version_count(self) %></span>
<% if CurrentUser.user.id == user.id || CurrentUser.is_janitor? %>
<span>Flags</span>
<span><%= presenter.flag_count(self) %></span>
<% end %>
<% if CurrentUser.user.id == user.id || CurrentUser.is_moderator? %>
<span>Tickets</span>
<span>
<%= presenter.ticket_count(self) %>
<% if CurrentUser.is_moderator? %>
[<%= link_to "pending", tickets_path(search: { creator_id: user.id, status: "pending" }) %>]
[<%= link_to "accused", tickets_path(search: { accused_id: user.id }) %>]
<% end %>
</span>
<% end %>
<% if CurrentUser.id == user.id %>
<span>API Key</span>
<span>
<%= link_to (CurrentUser.api_key ? "View" : "Generate"), user_api_key_path(CurrentUser.user) %>
(<%= link_to "help", help_page_path(id: "api") %>)
</span>
<% end %>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
<% if has_about %>
<tab-entry tab="about" class="profile-about-section flex">
<tab-head>About</tab-head>
<tab-body class="content dtext-container">
<%= format_text(user.profile_about, allow_color: true) %>
</tab-body>
</tab-entry>
<% end %>
<% if has_artinfo %>
<tab-entry tab="artinfo" class="profile-artinfo-section flex">
<tab-head>Artist Information</tab-head>
<tab-body class="content dtext-container">
<%= format_text(user.profile_artinfo, allow_color: true) %>
</tab-body>
</tab-entry>
<% end %>

View File

@ -0,0 +1,5 @@
<% if user.is_banned? && user.recent_ban %>
<div class="profile-ban dtext-container">
<%= format_text presenter.ban_reason %>
</div>
<% end %>

View File

@ -0,0 +1,20 @@
<div class="profile-card">
<div class="profile-avatar">
<%= profile_avatar(@user) %>
</div>
<div class="profile-info">
<span class="profile-name">
<%= link_to_user(@user) %>
<%= user_feedback_badge(@user) %>
</span>
<span class="profile-joined">
Joined <%= compact_date @user.created_at %>
</span>
<span class="profile-rank">
<%= user_level_badge(@user) %>
<% unless @user.is_verified? %>
<span class="level-badge level-unactivated">UNACTIVATED</span>
<% end %>
</span>
</div>
</div>

View File

@ -4,7 +4,7 @@
<div class="profile-sample profile-uploads">
<div class="profile-sample-header">
<%= link_to "Uploads", posts_path(:tags => "user:#{user.name}"), class: "title" %>
<%= link_to "Uploads", posts_path(tags: "user:#{user.name}"), class: "title" %>
</div>
<div class="profile-sample-links">
@ -29,11 +29,11 @@
<div class="profile-sample profile-favorites">
<div class="profile-sample-header">
<%= link_to "Favorites", favorites_path(:user_id => user.id), class: "title" %>
<%= link_to "Favorites", favorites_path(user_id: user.id), class: "title" %>
</div>
<div class="profile-sample-links">
<%= link_to(sanitize("<b>#{user.favorite_count}</b> total"), favorites_path(:user_id => user.id)) %>
<%= link_to(sanitize("<b>#{user.favorite_count}</b> total"), favorites_path(user_id: user.id)) %>
<% if user.enable_privacy_mode? || user.is_blocked? %>
<span>[hidden]</span>
<% end %>

View File

@ -0,0 +1,34 @@
<div class="profile-section hidden" name="StaffStats">
<div class="profile-section-header">Staff Info</div>
<div class="profile-section-body profile-staff-info">
<% if presenter.previous_names(self).present? %>
<h4 class="block">Previous Names</h4>
<span class="block"><%= presenter.previous_names(self) %> -> <%= user.name %></span>
<% end %>
<% if CurrentUser.is_admin? %>
<h4>Email</h4>
<span>
<%= user.email %>
<%= email_domain_search(user.email) %>
</span>
<h4>Last IP</h4>
<span><%= link_to_ip(user.last_ip_addr) %></span>
<% end %>
<% if CurrentUser.is_moderator? %>
<h4>Votes</h4>
<span>
<%= link_to "Posts", action: "index", controller: "post_votes", search: { user_name: user.name } %>
| <%= link_to "Comments", action: "index", controller: "comment_votes", search: { user_name: user.name } %>
</span>
<% end %>
<h4>Pending</h4>
<span>
<%= link_to "Posts", posts_path(tags: "user:#{user.name} status:pending") %>
| <%= link_to "Replacements", post_replacements_path(search: { creator_name: user.name }) %>
</span>
</div>
</div>

View File

@ -0,0 +1,129 @@
<tab-entry tab="stats" class="profile-user-info">
<div class="profile-line">
<h4><%= svg_icon(:message_square) %> Comments</h4>
<span class="profile-line-number">
<%= presenter.comment_count(self) %>
</span>
<span class="profile-line-extra">
<%= link_to "[mentions]", comments_path(group_by: :comment, search: { body_matches: user.name }) %>
</span>
</div>
<div class="profile-line">
<h4><%= svg_icon(:lectern) %> Forum posts</h4>
<span class="profile-line-number">
<%= presenter.forum_post_count(self) %>
</span>
<span class="profile-line-extra">
<%= link_to "[mentions]", forum_posts_path(search: { body_matches: user.name }) %>
</span>
</div>
<% if user.can_approve_posts? || Post.where(approver: user).exists? %>
<div class="profile-line">
<h4><%= svg_icon(:stamp) %> Approvals</h4>
<span class="profile-line-number">
<%= presenter.approval_count(self) %>
</span>
</div>
<% end %>
<div class="profile-line">
<h4><%= svg_icon(:upload) %> Upload Limit</h4>
<span class="profile-line-number">
<% if CurrentUser.user.id == user.id %>
<%= link_to presenter.upload_limit_short, upload_limit_users_path %>
<% else %>
<%= link_to presenter.upload_limit_short, wiki_page_path(id: "upload_limit") %>
<% end %>
</span>
<span class="profile-line-extra">
Max number of unapproved posts at a time.
</span>
</div>
<div class="profile-line">
<h4><%= svg_icon(:replace) %> Changes</h4>
<span class="profile-line-extra profile-line-show">
<% if user.post_update_count != 0 %>
<a href="<%= post_versions_path(lr: user.id, search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.post_update_count %></h5>
<span>Post</span>
</a>
<% end %>
<% if user.pool_version_count != 0 %>
<a href="<%= pool_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.pool_version_count %></h5>
<span>Pool</span>
</a>
<% end %>
<% if user.artist_version_count != 0 %>
<a href="<%= artist_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.artist_version_count %></h5>
<span>Artist</span>
</a>
<% end %>
<% if user.wiki_page_version_count != 0 %>
<a href="<%= wiki_page_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.wiki_page_version_count %></h5>
<span>Wiki</span>
</a>
<% end %>
<% if user.note_version_count != 0 %>
<a href="<%= note_versions_path(search: { updater_id: user.id }) %>" class="entry">
<h5><%= user.note_version_count %></h5>
<span>Note</span>
</a>
<% end %>
</span>
</div>
<% if CurrentUser.user.id == user.id || CurrentUser.is_approver? %>
<div class="profile-line">
<h4><%= svg_icon(:flag_left) %> Flags</h4>
<span class="profile-line-number"><%= presenter.flag_count(self) %></span>
</div>
<% end %>
<% if CurrentUser.user.id == user.id || CurrentUser.is_moderator? %>
<div class="profile-line">
<h4><%= svg_icon(:ticket) %> Tickets</h4>
<span class="profile-line-number"><%= presenter.ticket_count(self) %></span>
<% if CurrentUser.is_moderator? %>
<span class="profile-line-extra">
[<%= link_to "pending", tickets_path(search: { creator_id: user.id, status: "pending" }) %>]
[<%= link_to "accused", tickets_path(search: { accused_id: user.id }) %>]
</span>
<% end %>
</div>
<% end %>
<% if CurrentUser.user.id == user.id %>
<div class="profile-line">
<h4><%= svg_icon(:key_square) %> API Key</h4>
<span class="profile-line-number">
<%= link_to (CurrentUser.api_key ? "View" : "Generate"), user_api_key_path(CurrentUser.user) %>
</span>
<span class="profile-line-extra">
(<%= link_to "help", help_page_path(id: "api") %>)
</span>
</div>
<% end %>
<% permissions = presenter.permissions %>
<% unless permissions.empty? %>
<div class="profile-line">
<h4><%= svg_icon(:power) %> Permissions</h4>
<span class="profile-line-extra">
<%= permissions %>
</span>
</div>
<% end %>
</tab-entry>

View File

@ -1,10 +1,41 @@
<div id="c-users">
<div id="a-show">
<%= render "statistics", :presenter => @presenter, :user => @user %>
<!-- Header -->
<%= render "/users/partials/show/card", :presenter => @presenter, :user => @user %>
<%= render "/users/partials/show/ban_banner", :presenter => @presenter, :user => @user %>
<% if CurrentUser.is_staff? %>
<!-- Staff Info -->
<%= render "/users/partials/show/staff_info", :presenter => @presenter, :user => @user %>
<%= render "staff_notes/partials/for_user", user: @user %>
<% end %>
<!-- Blacklist -->
<%= render "posts/partials/common/inline_blacklist" %>
<%= render "post_summary", presenter: @presenter, user: @user %>
<%= render "about", presenter: @presenter, user: @user %>
<!-- Central panel -->
<% has_about = @user.profile_about.present? %>
<% has_artinfo = @user.profile_artinfo.present? %>
<tabs-menu
id="profile-tabs"
data-has-about="<%= has_about %>"
data-has-artinfo="<%= has_artinfo %>"
>
<% if has_about %>
<button role="tab" name="about">About</button>
<% end %>
<% if has_artinfo %>
<button role="tab" name="artinfo">Commission Info</button>
<% end %>
<button role="tab" name="stats">Stats</button>
</tabs-menu>
<tabs-content for="profile-tabs">
<%= render "/users/partials/show/user_info", :presenter => @presenter, :user => @user %>
<%= render "/users/partials/show/about", presenter: @presenter, user: @user, has_about: has_about, has_artinfo: has_artinfo %>
<%= render "/users/partials/show/post_summary", presenter: @presenter, user: @user %>
</tabs-content>
</div>
</div>