From 0dd17474aa69bddd1f00b29c25f699ae1ee69793 Mon Sep 17 00:00:00 2001 From: Cinder Date: Wed, 26 Feb 2025 05:34:43 -0800 Subject: [PATCH] [Users] Rework the profile page (#931) --- app/helpers/application_helper.rb | 19 +-- app/helpers/icon_helper.rb | 10 ++ app/helpers/users_helper.rb | 43 +++++ app/javascript/src/javascripts/thumbnails.js | 8 +- app/javascript/src/javascripts/users.js | 25 +++ .../src/javascripts/utility/storage.js | 11 ++ app/javascript/src/styles/base.scss | 1 + .../styles/common/_standard_variables.scss | 4 +- .../src/styles/common/navigation.scss | 22 +-- app/javascript/src/styles/specific/users.scss | 102 +----------- .../views/application/_application.scss | 3 + .../src/styles/views/application/_avatar.scss | 41 +++++ .../views/application/_feedback_badge.scss | 103 ++++++++++++ .../views/application/_level_badge.scss | 40 +++++ .../src/styles/views/users/_users.scss | 1 + .../src/styles/views/users/edit/_edit.scss | 5 +- .../src/styles/views/users/show/_show.scss | 10 ++ .../views/users/show/partials/_about.scss | 44 +++++ .../users/show/partials/_ban_banner.scss | 9 + .../views/users/show/partials/_card.scss | 87 ++++++++++ .../users/show/partials/_post_summary.scss | 107 ++++++++++++ .../users/show/partials/_profile_section.scss | 35 ++++ .../users/show/partials/_staff_info.scss | 16 ++ .../views/users/show/partials/_user_info.scss | 71 ++++++++ app/models/user.rb | 27 +++ app/presenters/user_presenter.rb | 5 + app/views/application/_feedback_badge.erb | 34 ++++ app/views/application/_profile_avatar.erb | 11 ++ app/views/layouts/_nav.html.erb | 6 +- .../staff_notes/partials/_for_user.html.erb | 21 +-- app/views/users/_about.html.erb | 14 -- app/views/users/_statistics.html.erb | 156 ------------------ app/views/users/partials/show/_about.html.erb | 17 ++ .../users/partials/show/_ban_banner.html.erb | 5 + app/views/users/partials/show/_card.html.erb | 20 +++ .../users/partials/show/_mentions.html.erb | 0 .../show}/_post_summary.html.erb | 6 +- .../users/partials/show/_staff_info.html.erb | 34 ++++ .../users/partials/show/_user_info.html.erb | 129 +++++++++++++++ app/views/users/show.html.erb | 39 ++++- 40 files changed, 1016 insertions(+), 325 deletions(-) create mode 100644 app/javascript/src/javascripts/users.js create mode 100644 app/javascript/src/styles/views/application/_application.scss create mode 100644 app/javascript/src/styles/views/application/_avatar.scss create mode 100644 app/javascript/src/styles/views/application/_feedback_badge.scss create mode 100644 app/javascript/src/styles/views/application/_level_badge.scss create mode 100644 app/javascript/src/styles/views/users/show/_show.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_about.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_ban_banner.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_card.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_post_summary.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_profile_section.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_staff_info.scss create mode 100644 app/javascript/src/styles/views/users/show/partials/_user_info.scss create mode 100644 app/views/application/_feedback_badge.erb create mode 100644 app/views/application/_profile_avatar.erb delete mode 100644 app/views/users/_about.html.erb delete mode 100644 app/views/users/_statistics.html.erb create mode 100644 app/views/users/partials/show/_about.html.erb create mode 100644 app/views/users/partials/show/_ban_banner.html.erb create mode 100644 app/views/users/partials/show/_card.html.erb create mode 100644 app/views/users/partials/show/_mentions.html.erb rename app/views/users/{ => partials/show}/_post_summary.html.erb (89%) create mode 100644 app/views/users/partials/show/_staff_info.html.erb create mode 100644 app/views/users/partials/show/_user_info.html.erb diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 2f6b5a361..38c7f195c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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})" diff --git a/app/helpers/icon_helper.rb b/app/helpers/icon_helper.rb index fc5bc05fa..e9f2c77e6 100644 --- a/app/helpers/icon_helper.rb +++ b/app/helpers/icon_helper.rb @@ -20,8 +20,18 @@ module IconHelper user: %(), # Utility + plus: %(), times: %(), reset: %(), + replace: %(), + upload: %(), + stamp: %(), + power: %(), + circle_help: %(), + notepad: %(), + flag_left: %(), + ticket: %(), + key_square: %(), # Pagination chevron_left: %(), diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 161233348..a0e8abe2b 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -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 diff --git a/app/javascript/src/javascripts/thumbnails.js b/app/javascript/src/javascripts/thumbnails.js index cfad7cadb..958ee9f1b 100644 --- a/app/javascript/src/javascripts/thumbnails.js +++ b/app/javascript/src/javascripts/thumbnails.js @@ -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 () { $("") .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; } diff --git a/app/javascript/src/javascripts/users.js b/app/javascript/src/javascripts/users.js new file mode 100644 index 000000000..c95c0b7c3 --- /dev/null +++ b/app/javascript/src/javascripts/users.js @@ -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))); +}); diff --git a/app/javascript/src/javascripts/utility/storage.js b/app/javascript/src/javascripts/utility/storage.js index 601b98d60..bd81923c4 100644 --- a/app/javascript/src/javascripts/utility/storage.js +++ b/app/javascript/src/javascripts/utility/storage.js @@ -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 diff --git a/app/javascript/src/styles/base.scss b/app/javascript/src/styles/base.scss index 80a7cebe8..2a7a3db2d 100644 --- a/app/javascript/src/styles/base.scss +++ b/app/javascript/src/styles/base.scss @@ -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"; diff --git a/app/javascript/src/styles/common/_standard_variables.scss b/app/javascript/src/styles/common/_standard_variables.scss index 8504ec0b5..1dcadec00 100644 --- a/app/javascript/src/styles/common/_standard_variables.scss +++ b/app/javascript/src/styles/common/_standard_variables.scss @@ -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); } diff --git a/app/javascript/src/styles/common/navigation.scss b/app/javascript/src/styles/common/navigation.scss index 582caaa0b..54c417d12 100644 --- a/app/javascript/src/styles/common/navigation.scss +++ b/app/javascript/src/styles/common/navigation.scss @@ -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; } } } } diff --git a/app/javascript/src/styles/specific/users.scss b/app/javascript/src/styles/specific/users.scss index c84173453..84b7eff06 100644 --- a/app/javascript/src/styles/specific/users.scss +++ b/app/javascript/src/styles/specific/users.scss @@ -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 diff --git a/app/javascript/src/styles/views/application/_application.scss b/app/javascript/src/styles/views/application/_application.scss new file mode 100644 index 000000000..287a1081c --- /dev/null +++ b/app/javascript/src/styles/views/application/_application.scss @@ -0,0 +1,3 @@ +@import "avatar"; +@import "feedback_badge"; +@import "level_badge"; diff --git a/app/javascript/src/styles/views/application/_avatar.scss b/app/javascript/src/styles/views/application/_avatar.scss new file mode 100644 index 000000000..fc73dd740 --- /dev/null +++ b/app/javascript/src/styles/views/application/_avatar.scss @@ -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; + } + } +} diff --git a/app/javascript/src/styles/views/application/_feedback_badge.scss b/app/javascript/src/styles/views/application/_feedback_badge.scss new file mode 100644 index 000000000..f796149ea --- /dev/null +++ b/app/javascript/src/styles/views/application/_feedback_badge.scss @@ -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"); } + } + } +} diff --git a/app/javascript/src/styles/views/application/_level_badge.scss b/app/javascript/src/styles/views/application/_level_badge.scss new file mode 100644 index 000000000..b61941516 --- /dev/null +++ b/app/javascript/src/styles/views/application/_level_badge.scss @@ -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; + } +} diff --git a/app/javascript/src/styles/views/users/_users.scss b/app/javascript/src/styles/views/users/_users.scss index 1fe06b79f..46f889bd9 100644 --- a/app/javascript/src/styles/views/users/_users.scss +++ b/app/javascript/src/styles/views/users/_users.scss @@ -1 +1,2 @@ +@import "show/show"; @import "edit/edit"; diff --git a/app/javascript/src/styles/views/users/edit/_edit.scss b/app/javascript/src/styles/views/users/edit/_edit.scss index 128c1d984..785c99a0b 100644 --- a/app/javascript/src/styles/views/users/edit/_edit.scss +++ b/app/javascript/src/styles/views/users/edit/_edit.scss @@ -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; diff --git a/app/javascript/src/styles/views/users/show/_show.scss b/app/javascript/src/styles/views/users/show/_show.scss new file mode 100644 index 000000000..67421bc05 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/_show.scss @@ -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"; +} diff --git a/app/javascript/src/styles/views/users/show/partials/_about.scss b/app/javascript/src/styles/views/users/show/partials/_about.scss new file mode 100644 index 000000000..027388d94 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_about.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/javascript/src/styles/views/users/show/partials/_ban_banner.scss b/app/javascript/src/styles/views/users/show/partials/_ban_banner.scss new file mode 100644 index 000000000..5f867e327 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_ban_banner.scss @@ -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; + } +} diff --git a/app/javascript/src/styles/views/users/show/partials/_card.scss b/app/javascript/src/styles/views/users/show/partials/_card.scss new file mode 100644 index 000000000..b3d3f03b0 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_card.scss @@ -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"); } + } +} diff --git a/app/javascript/src/styles/views/users/show/partials/_post_summary.scss b/app/javascript/src/styles/views/users/show/partials/_post_summary.scss new file mode 100644 index 000000000..c4fbe6183 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_post_summary.scss @@ -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; + } + } +} diff --git a/app/javascript/src/styles/views/users/show/partials/_profile_section.scss b/app/javascript/src/styles/views/users/show/partials/_profile_section.scss new file mode 100644 index 000000000..2cbbeefd4 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_profile_section.scss @@ -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; + } + } +} diff --git a/app/javascript/src/styles/views/users/show/partials/_staff_info.scss b/app/javascript/src/styles/views/users/show/partials/_staff_info.scss new file mode 100644 index 000000000..096717352 --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_staff_info.scss @@ -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; + } +} diff --git a/app/javascript/src/styles/views/users/show/partials/_user_info.scss b/app/javascript/src/styles/views/users/show/partials/_user_info.scss new file mode 100644 index 000000000..8d744b44f --- /dev/null +++ b/app/javascript/src/styles/views/users/show/partials/_user_info.scss @@ -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; } + } +} diff --git a/app/models/user.rb b/app/models/user.rb index 0ad1ecde7..871c61196 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb index 5a4ab4662..4c7260f67 100644 --- a/app/presenters/user_presenter.rb +++ b/app/presenters/user_presenter.rb @@ -53,6 +53,11 @@ class UserPresenter = #{user.upload_limit}}.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) diff --git a/app/views/application/_feedback_badge.erb b/app/views/application/_feedback_badge.erb new file mode 100644 index 000000000..47375347a --- /dev/null +++ b/app/views/application/_feedback_badge.erb @@ -0,0 +1,34 @@ + + <% if deleted > 0 %> + <%= deleted %> + <% end %> + + <% if negative > 0 %> + <%= negative %> + <% end %> + + <% if neutral > 0 %> + <%= neutral %> + <% end %> + + <% if positive > 0 %> + <%= positive %> + <% end %> + + +<% if CurrentUser.is_moderator? && CurrentUser.user != user && active == 0 %> + " + class="user-records-list" + title="New Feedback" + > + <%= svg_icon(:plus) %> + +<% end %> \ No newline at end of file diff --git a/app/views/application/_profile_avatar.erb b/app/views/application/_profile_avatar.erb new file mode 100644 index 000000000..584e1255b --- /dev/null +++ b/app/views/application/_profile_avatar.erb @@ -0,0 +1,11 @@ +" + data-id="<%= post_id %>" + data-name="<%= user.name %>" +> + + diff --git a/app/views/layouts/_nav.html.erb b/app/views/layouts/_nav.html.erb index d306a09b1..d04d9ff6b 100644 --- a/app/views/layouts/_nav.html.erb +++ b/app/views/layouts/_nav.html.erb @@ -20,11 +20,11 @@ <% if CurrentUser.is_anonymous? %> -
-
- Staff Notes (<%= user.staff_notes.count %>) -
-

<%= link_to "Staff Notes", staff_notes_path(search: { user_id: user.id }) %>

- <%= render "staff_notes/partials/list_of_notes", staff_notes: user.staff_notes.limit(15), show_receiver_name: false %> -
-

<%= link_to "Create »", new_staff_note_path(search: { user_id: user.id }), class: "expand-new-staff-note" %>

- <%= render "staff_notes/partials/new", user: user, staff_note: StaffNote.new(user_id: user.id), hidden: true %> -
-
-
+ <% end %> diff --git a/app/views/users/_about.html.erb b/app/views/users/_about.html.erb deleted file mode 100644 index faa72ecad..000000000 --- a/app/views/users/_about.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -
- <% if user.profile_about.present? %> -
-

About

-
<%= format_text(user.profile_about, allow_color: true) %>
-
- <% end %> - <% if user.profile_artinfo.present? %> -
-

Artist Information

-
<%= format_text(user.profile_artinfo, allow_color: true) %>
-
- <% end %> -
diff --git a/app/views/users/_statistics.html.erb b/app/views/users/_statistics.html.erb deleted file mode 100644 index 5c230162c..000000000 --- a/app/views/users/_statistics.html.erb +++ /dev/null @@ -1,156 +0,0 @@ -
-
- <%= user_avatar @user %> -
-
-

<%= link_to_user @user %>

-
-
- Join Date - <%= compact_time @user.created_at %> - - Level - <%= "(Unactivated)" unless user.is_verified? %> <%= presenter.level %> - - <% if user.is_banned? && user.recent_ban %> - Ban reason - <%= format_text presenter.ban_reason %> - <% end %> - - Posts - - <%= 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 %> - - - Deleted - - <%= presenter.deleted_upload_count(self) %> - - - Replaced - - <%= presenter.replaced_upload_count(self) %> - [<%= link_to "pending", post_replacements_path(search: { creator_name: user.name }) %>] - - - Rejected - <%= presenter.rejected_replacements_count(self) %> - - Favorites - - <%= presenter.favorite_count(self) %> - - - Forum Posts - - <%= presenter.forum_post_count(self) %> - (<%= link_to "mentions", forum_posts_path(search: { body_matches: user.name }) %>) - - - Comments - - <%= 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 %> - - - <% if user.can_approve_posts? || Post.where(approver: user).exists? %> - Approvals - <%= presenter.approval_count(self) %> - <% end %> - - <% if CurrentUser.user.id == user.id || CurrentUser.is_janitor? %> - <% if presenter.previous_names(self).present? %> - Previous Names - <%= presenter.previous_names(self) %> -> <%= user.name %> - <% end %> - <% end %> - - <% if CurrentUser.is_admin? %> - Email - - <%= user.email %> - <%= email_domain_search(user.email) %> - - Last IP - <%= link_to_ip(user.last_ip_addr) %> - <% end %> -
- -
- Feedback - - <%= 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 %> - - - Permissions - <%= presenter.permissions %> - - Upload Limit - - <%= 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 %> - - - Post Changes - - <%= 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 %> - - - Wiki Changes - <%= presenter.wiki_page_version_count(self) %> - - Note Changes - <%= presenter.note_version_count(self) %> on <%= presenter.noted_posts_count(self) %> posts - - Artist Changes - <%= presenter.artist_version_count(self) %> - - Pool Changes - <%= presenter.pool_version_count(self) %> - - <% if CurrentUser.user.id == user.id || CurrentUser.is_janitor? %> - Flags - <%= presenter.flag_count(self) %> - <% end %> - - <% if CurrentUser.user.id == user.id || CurrentUser.is_moderator? %> - Tickets - - <%= 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 %> - - <% end %> - - <% if CurrentUser.id == user.id %> - API Key - - <%= link_to (CurrentUser.api_key ? "View" : "Generate"), user_api_key_path(CurrentUser.user) %> - (<%= link_to "help", help_page_path(id: "api") %>) - - <% end %> -
-
-
-
diff --git a/app/views/users/partials/show/_about.html.erb b/app/views/users/partials/show/_about.html.erb new file mode 100644 index 000000000..636f651dd --- /dev/null +++ b/app/views/users/partials/show/_about.html.erb @@ -0,0 +1,17 @@ +<% if has_about %> + + About + + <%= format_text(user.profile_about, allow_color: true) %> + + +<% end %> + +<% if has_artinfo %> + + Artist Information + + <%= format_text(user.profile_artinfo, allow_color: true) %> + + +<% end %> diff --git a/app/views/users/partials/show/_ban_banner.html.erb b/app/views/users/partials/show/_ban_banner.html.erb new file mode 100644 index 000000000..a6ffe4218 --- /dev/null +++ b/app/views/users/partials/show/_ban_banner.html.erb @@ -0,0 +1,5 @@ +<% if user.is_banned? && user.recent_ban %> +
+ <%= format_text presenter.ban_reason %> +
+<% end %> diff --git a/app/views/users/partials/show/_card.html.erb b/app/views/users/partials/show/_card.html.erb new file mode 100644 index 000000000..e635ce1f0 --- /dev/null +++ b/app/views/users/partials/show/_card.html.erb @@ -0,0 +1,20 @@ +
+
+ <%= profile_avatar(@user) %> +
+
+ + <%= link_to_user(@user) %> + <%= user_feedback_badge(@user) %> + + + Joined <%= compact_date @user.created_at %> + + + <%= user_level_badge(@user) %> + <% unless @user.is_verified? %> + UNACTIVATED + <% end %> + +
+
diff --git a/app/views/users/partials/show/_mentions.html.erb b/app/views/users/partials/show/_mentions.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/users/_post_summary.html.erb b/app/views/users/partials/show/_post_summary.html.erb similarity index 89% rename from app/views/users/_post_summary.html.erb rename to app/views/users/partials/show/_post_summary.html.erb index 2c4873a71..886cf76f5 100644 --- a/app/views/users/_post_summary.html.erb +++ b/app/views/users/partials/show/_post_summary.html.erb @@ -4,7 +4,7 @@
- <%= link_to "Uploads", posts_path(:tags => "user:#{user.name}"), class: "title" %> + <%= link_to "Uploads", posts_path(tags: "user:#{user.name}"), class: "title" %>