diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 6fe8362c4..f971ec82a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -19,6 +19,12 @@ class PostsController < ApplicationController @query = tag_query.nil? ? [] : tag_query.strip.split(/ /, 2).compact_blank if @query.length == 1 @wiki_page = WikiPage.titled(@query[0]) + + # redirect? + if @wiki_page.present? && @wiki_page.parent.present? + @wiki_page = WikiPage.titled(@wiki_page.parent) + end + @wiki_text = @wiki_page.present? ? @wiki_page.body : "" if @wiki_text.present? @wiki_text = @wiki_text diff --git a/app/controllers/wiki_pages_controller.rb b/app/controllers/wiki_pages_controller.rb index d586e4d9b..74b533b2c 100644 --- a/app/controllers/wiki_pages_controller.rb +++ b/app/controllers/wiki_pages_controller.rb @@ -123,9 +123,10 @@ class WikiPagesController < ApplicationController end def wiki_page_params(context) - permitted_params = %i[body edit_reason] + permitted_params = %i[body category_id edit_reason] permitted_params += %i[parent] if CurrentUser.is_privileged? permitted_params += %i[is_locked is_deleted skip_secondary_validations] if CurrentUser.is_janitor? + permitted_params += %i[category_is_locked] if CurrentUser.is_admin? permitted_params += %i[title] if context == :create || CurrentUser.is_janitor? params.fetch(:wiki_page, {}).permit(permitted_params) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f3d6ecfd5..e725f8501 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: home_users_path, class: "simple-avatar #{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 user_banner(user) return "" if user.nil? post_id = user.banner_id diff --git a/app/helpers/icon_helper.rb b/app/helpers/icon_helper.rb index 077752253..e9f2c77e6 100644 --- a/app/helpers/icon_helper.rb +++ b/app/helpers/icon_helper.rb @@ -17,10 +17,21 @@ module IconHelper swatch: %(), settings: %(), log_in: %(), + user: %(), # Utility + plus: %(), times: %(), reset: %(), + replace: %(), + upload: %(), + stamp: %(), + power: %(), + circle_help: %(), + notepad: %(), + flag_left: %(), + ticket: %(), + key_square: %(), # Pagination chevron_left: %(), @@ -28,7 +39,9 @@ module IconHelper ellipsis: %(), # Posts + search: %(), fullscreen: %(), + anchor: %(), }.freeze def svg_icon(name, *args) diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index d293d422a..8b55b9e01 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -1,6 +1,29 @@ # frozen_string_literal: true module PaginationHelper + def approximate_count(records) + return "" if records.pagination_mode != :numbered + + if records.total_pages > records.max_numbered_pages + pages = records.max_numbered_pages + schar = "over " + count = pages * records.records_per_page + title = "Over #{count} results found.\nActual result count may be much larger." + else + pages = records.total_pages + schar = "~" + count = pages * records.records_per_page + title = "Approximately #{count} results found.\nActual result count may differ." + end + + tag.span(class: "approximate-count", title: title, data: { count: count, pages: pages, per: records.max_numbered_pages }) do + concat schar + concat number_to_human(count, precision: 2, format: "%n%u", units: { thousand: "k" }) + concat " " + concat "result".pluralize(count) + end + end + def sequential_paginator(records) tag.nav(class: "pagination sequential", aria: { label: "Pagination" }) do return "" if records.try(:none?) @@ -64,12 +87,12 @@ module PaginationHelper html = "".html_safe if disabled - html << tag.span(class: "next", id: "paginator-next", data: { shortcut: "a left" }) do + html << tag.span(class: "next", id: "paginator-next", data: { shortcut: "d right" }) do concat tag.span("Next") concat svg_icon(:chevron_right) end else - html << link_to(link, class: "next", id: "paginator-prev", rel: "next", data: { shortcut: "a left" }) do + html << link_to(link, class: "next", id: "paginator-next", rel: "next", data: { shortcut: "d right" }) do concat tag.span("Next") concat svg_icon(:chevron_right) end 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/post_search.js b/app/javascript/src/javascripts/post_search.js index b5b3e6a17..045d641d8 100644 --- a/app/javascript/src/javascripts/post_search.js +++ b/app/javascript/src/javascripts/post_search.js @@ -67,6 +67,13 @@ PostSearch.initialize_controls = function () { $("body").attr("data-st-fullscreen", fullscreen); LStorage.Posts.Fullscreen = fullscreen; }); + + let stickySearch = LStorage.Posts.StickySearch; + $("#search-sticky").on("click", () => { + stickySearch = !stickySearch; + $("body").attr("data-st-ssearch", stickySearch); + LStorage.Posts.StickySearch = stickySearch; + }); }; $(() => { diff --git a/app/javascript/src/javascripts/tabs.js b/app/javascript/src/javascripts/tabs.js new file mode 100644 index 000000000..ef0f4e26e --- /dev/null +++ b/app/javascript/src/javascripts/tabs.js @@ -0,0 +1,170 @@ +class Tabs { + + constructor ($element) { + this.$menu = $element; + + const id = this.$menu.attr("id"); + const pagesWrap = $(`tabs-content[for="${id}"]`); + if (!pagesWrap) { + console.error("E6.Tabs", "No content"); + return; + } + + // Create page and search indices + this.index = new TabIndex(pagesWrap, this.$menu); + + // Bootstrap search + const input = this.$menu.find("input[name='search']").on("input", (event, crabs) => { + const val = (input.val() + "").trim(); + this.index.find(val); + + // Set the query param + if (crabs) return; + + const url = new URL(window.location); + if (!val) url.searchParams.delete("find"); + else url.searchParams.set("find", input.val() + ""); + url.searchParams.delete("tab"); + + window.history.pushState({}, "", url); + }); + + // Bootstrap tab buttons + const firstButtonName = this.$menu.find("button").first().attr("name"); + this.$menu.on("click", "button", (event, crabs) => { + const button = $(event.currentTarget); + const name = button.attr("name"); + if (!name) return; + + // Toggle the page + this.index.openPage(name); + this.$menu.find("button.active").removeClass("active"); + button.addClass("active"); + input.val(""); + + // Set the query param + if (crabs) return; + + const url = new URL(window.location); + if (name == firstButtonName) url.searchParams.delete("tab"); + else url.searchParams.set("tab", name); + url.searchParams.delete("find"); + + window.history.pushState({}, "", url); + }); + + // Attempt to restore previous state + const queryParams = new URLSearchParams(window.location.search); + if (queryParams.get("tab")) { + // No error handling: if the button does not exist nothing loads + this.$menu.find(`button[name="${queryParams.get("tab")}"]`).trigger("click", [ true ]); + input.val(""); + } else if (queryParams.get("find")) { + input.val(queryParams.get("find"), [ true ]); + input.trigger("input"); + } else { + // Just open the first tab + this.$menu.find("button").first().click(); + input.val(""); + } + } +} + +class TabIndex { + + constructor (wrapper, $menu) { + + this.$menu = $menu; + + this.pages = {}; + this.search = {}; + this._allEntries = wrapper.children("tab-entry"); + for (const one of this._allEntries) { + const $one = $(one); + + const tab = $one.attr("tab"); + if (tab) { + if (!this.pages[tab]) this.pages[tab] = []; + this.pages[tab].push($one); + } + + // TODO Not a great way of doing this. + // Entries with the same search string will get overwriten. + const search = $one.attr("search"); + if (search) this.search[search] = $one; + } + + this.groups = {}; + this._allGroups = wrapper.children("tab-group"); + for (const one of this._allGroups) { + const $one = $(one); + const name = $one.attr("name"); + if (!name) return; + this.groups[name] = $one; + } + } + + + /** + * Show all entries on a specific tab + * @param {string} name Tab name + */ + openPage (name) { + + if (!name || !this.pages[name]) { + console.error("E6.Tabs", name, "does not exist"); + return; + } + + // Activate tab entries + const groups = new Set(); + this._allEntries.removeClass("active"); + for (const entry of this.pages[name]) { + entry.addClass("active"); + if (entry.attr("group")) + groups.add(entry.attr("group")); + } + + // Activate group headers + this._allGroups.removeClass("active"); + for (const group of groups) + this.groups[group].addClass("active"); + } + + + /** + * Find settings inputs based on keywords + * @param {string} query Search query + */ + find (query) { + this._allEntries.removeClass("active"); + this._allGroups.removeClass("active"); + + // Restore the previous session + if (query.length == 0) { + this.$menu.find("button").first().trigger("click", [ false ]); + return; + } + + const terms = query.split(" "); + const groups = new Set(); + for (const [tags, $element] of Object.entries(this.search)) { + for (const term of terms) { + if (!tags.includes(term)) continue; + $element.addClass("active"); + if ($element.attr("group")) + groups.add($element.attr("group")); + } + } + + // Activate group headers + this._allGroups.removeClass("active"); + for (const group of groups) + this.groups[group].addClass("active"); + } +} + +$(() => { + for (const one of $("tabs-menu")) + new Tabs($(one)); +}); diff --git a/app/javascript/src/javascripts/takedowns.js b/app/javascript/src/javascripts/takedowns.js index 54335d012..437c5a19c 100644 --- a/app/javascript/src/javascripts/takedowns.js +++ b/app/javascript/src/javascripts/takedowns.js @@ -39,8 +39,8 @@ Takedown.add_posts_by_tags_preview = function (id) { $("#takedown-add-posts-tags-warning").html(preview_text).show(); $("#takedown-add-posts-tags").prop("disabled", true); $("#takedown-add-posts-tags-preview").hide(); - $("#takedown-add-posts-tags-confirm").show(); - $("#takedown-add-posts-tags-cancel").show(); + $("#takedown-add-posts-tags-confirm").css("display", "inline-block"); + $("#takedown-add-posts-tags-cancel").css("display", "inline-block"); }).fail(function (data) { Utility.error(data.responseText); }); diff --git a/app/javascript/src/javascripts/themes.js b/app/javascript/src/javascripts/themes.js index a329e109e..d57c28153 100644 --- a/app/javascript/src/javascripts/themes.js +++ b/app/javascript/src/javascripts/themes.js @@ -3,17 +3,23 @@ import LStorage from "./utility/storage"; const Theme = {}; -Theme.Values = ["Main", "Extra", "StickyHeader", "ForumNotif", "Palette", "Navbar", "Gestures"]; +Theme.Values = { + "Theme": ["Main", "Extra", "Palette", "Font", "StickyHeader", "Navbar", "Gestures", "ForumNotif"], + "Posts": ["WikiExcerpt", "StickySearch"], +}; -for (const one of Theme.Values) { - Object.defineProperty(Theme, one, { - get () { return LStorage.Theme[one]; }, - set (value) { - // No value checking, we die like men - LStorage.Theme[one] = value; - $("body").attr("data-th-" + one.toLowerCase(), value); - }, - }); +for (const [label, settings] of Object.entries(Theme.Values)) { + for (const one of settings) { + Object.defineProperty(Theme, one, { + get () { return LStorage.Theme[one]; }, + set (value) { + // This has the unintended side effect of setting + // attribute values that don't exist on the body. + LStorage[label][one] = value; + $("body").attr("data-th-" + one.toLowerCase(), value); + }, + }); + } } Theme.initialize_selector = function () { @@ -24,13 +30,15 @@ Theme.initialize_selector = function () { return false; } - for (const one of Theme.Values) { - $("#theme_" + one.toLowerCase()) - .val(LStorage.Theme[one] + "") - .on("change", (event) => { - const data = event.target.value; - Theme[one] = data; - }); + for (const [label, settings] of Object.entries(Theme.Values)) { + for (const one of settings) + $(`#${label}_${one}`) + .val(LStorage[label][one] + "") + .on("change", (event) => { + const data = event.target.value; + console.log("change", one, data); + Theme[one] = data; + }); } }; @@ -42,12 +50,6 @@ Theme.initialize_buttons = function () { LStorage.Site.Mascot = 0; $("#mascot-value").text(LStorage.Site.Mascot); }); - - $("#wiki-excerpt-value").text(LStorage.Posts.WikiExcerpt); - $("#wiki-excerpt-reset").on("click", () => { - LStorage.Posts.WikiExcerpt = 1; - $("#wiki-excerpt-value").text(LStorage.Posts.WikiExcerpt); - }); }; $(() => { diff --git a/app/javascript/src/javascripts/thumbnails.js b/app/javascript/src/javascripts/thumbnails.js index 2ab79d685..76dd824b0 100644 --- a/app/javascript/src/javascripts/thumbnails.js +++ b/app/javascript/src/javascripts/thumbnails.js @@ -9,8 +9,9 @@ Thumbnails.initialize = function () { const replacedPosts = []; // Avatar special case - for (const post of $(".simple-avatar")) { + for (const post of $(".simple-avatar.placeholder, .profile-avatar.placeholder")) { const $post = $(post); + $post.removeClass("placeholder"); const postID = $post.data("id"); if (!postID) continue; @@ -20,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 e32c2a283..c99167d9a 100644 --- a/app/javascript/src/javascripts/utility/storage.js +++ b/app/javascript/src/javascripts/utility/storage.js @@ -57,6 +57,9 @@ LStorage.Theme = { /** @returns {string} Colorblind-friendly palette (default / deut / trit) */ Palette: ["theme-palette", "default"], + /** @returns {string} Font family (verdana / leto / lexend / dyslexic ) */ + Font: ["theme-font", "Verdana"], + /** @returns {string} Position of the navbar on the post page (top / bottom / both / none) */ Navbar: ["theme-nav", "top"], @@ -91,6 +94,9 @@ LStorage.Posts = { /** @returns {boolean} True if the search should be displayed in fullscreen */ Fullscreen: ["e6.posts.fusk", false], + + /** @returns {boolean} True if the search should be displayed in fullscreen */ + StickySearch: ["e6.posts.ssearch", false], }; StorageUtils.bootstrapMany(LStorage.Posts); @@ -184,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 6ec5cfc2f..2a7a3db2d 100644 --- a/app/javascript/src/styles/base.scss +++ b/app/javascript/src/styles/base.scss @@ -8,6 +8,7 @@ @import "base/base"; @import "base/links"; @import "base/fontawesome"; +@import "base/fonts"; @import "common/standard_variables"; @import "common/standard_elements"; @@ -42,6 +43,10 @@ @import "common/user_styles.scss"; @import "common/voting.scss"; +@import "views/application/application"; +@import "views/posts/posts"; +@import "views/users/users"; + @import "specific/admin_dashboards.scss"; @import "specific/api_keys.scss"; @import "specific/artists.scss"; @@ -68,8 +73,6 @@ @import "specific/popular.scss"; @import "specific/post_delete.scss"; @import "specific/post_flags.scss"; -@import "specific/post_index.scss"; -@import "specific/post_mode_menu.scss"; @import "specific/posts.scss"; @import "specific/post_replacements.scss"; @import "specific/post_versions.scss"; diff --git a/app/javascript/src/styles/base/_fonts.scss b/app/javascript/src/styles/base/_fonts.scss new file mode 100644 index 000000000..511b14388 --- /dev/null +++ b/app/javascript/src/styles/base/_fonts.scss @@ -0,0 +1,161 @@ +// Picker +body { + &[data-th-font="Lato"] { font-family: "Lato", $base_font_family; } + &[data-th-font="Lexend"] { font-family: "Lexend", $base_font_family; } + &[data-th-font="Monospace"] { font-family: monospace, monospace; } + &[data-th-font="OpenDyslexic"] { font-family: "OpenDyslexic", $base_font_family; } + &[data-th-font="OpenSans"] { font-family: "OpenSans", $base_font_family; } + &[data-th-font="ComicSans"] { font-family: "Comic Sans MS", "Comic Sans", cursive; } +} + +/** Font Definitions **/ +// Lato +@font-face { + font-family: "Lato"; + src: url("/public/fonts/Lato/Lato-Regular.woff2") format("woff2"), + url("/public/fonts/Lato/Lato-Regular.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Lato"; + src: url("/public/fonts/Lato/Lato-Bold.woff2") format("woff2"), + url("/public/fonts/Lato/Lato-Bold.woff") format("woff"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Lato"; + src: url("/public/fonts/Lato/Lato-Italic.woff2") format("woff2"), + url("/public/fonts/Lato/Lato-Italic.woff") format("woff"); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Lato"; + src: url("/public/fonts/Lato/Lato-BoldItalic.woff2") format("woff2"), + url("/public/fonts/Lato/Lato-BoldItalic.woff") format("woff"); + font-weight: bold; + font-style: italic; + font-display: swap; +} + + +// Lexend +@font-face { + font-family: "Lexend"; + src: url("/public/fonts/Lexend/Lexend-Regular.woff2") format("woff2"), + url("/public/fonts/Lexend/Lexend-Regular.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Lexend"; + src: url("/public/fonts/Lexend/Lexend-Bold.woff2") format("woff2"), + url("/public/fonts/Lexend/Lexend-Bold.woff") format("woff"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Lexend"; + src: url("/public/fonts/Lexend/Lexend-Italic.woff2") format("woff2"), + url("/public/fonts/Lexend/Lexend-Italic.woff") format("woff"); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "Lexend"; + src: url("/public/fonts/Lexend/Lexend-BoldItalic.woff2") format("woff2"), + url("/public/fonts/Lexend/Lexend-BoldItalic.woff") format("woff"); + font-weight: bold; + font-style: italic; + font-display: swap; +} + + +// OpenDyslexic +@font-face { + font-family: "OpenDyslexic"; + src: url("/public/fonts/OpenDyslexic/OpenDyslexic-Regular.woff2") format("woff2"), + url("/public/fonts/OpenDyslexic/OpenDyslexic-Regular.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "OpenDyslexic"; + src: url("/public/fonts/OpenDyslexic/OpenDyslexic-Bold.woff2") format("woff2"), + url("/public/fonts/OpenDyslexic/OpenDyslexic-Bold.woff") format("woff"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "OpenDyslexic"; + src: url("/public/fonts/OpenDyslexic/OpenDyslexic-Italic.woff2") format("woff2"), + url("/public/fonts/OpenDyslexic/OpenDyslexic-Italic.woff") format("woff"); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "OpenDyslexic"; + src: url("/public/fonts/OpenDyslexic/OpenDyslexic-BoldItalic.woff2") format("woff2"), + url("/public/fonts/OpenDyslexic/OpenDyslexic-BoldItalic.woff") format("woff"); + font-weight: bold; + font-style: italic; + font-display: swap; +} + + +// OpenSans +@font-face { + font-family: "OpenSans"; + src: url("/public/fonts/OpenSans/OpenSans-Regular.woff2") format("woff2"), + url("/public/fonts/OpenSans/OpenSans-Regular.woff") format("woff"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "OpenSans"; + src: url("/public/fonts/OpenSans/OpenSans-Bold.woff2") format("woff2"), + url("/public/fonts/OpenSans/OpenSans-Bold.woff") format("woff"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "OpenSans"; + src: url("/public/fonts/OpenSans/OpenSans-Italic.woff2") format("woff2"), + url("/public/fonts/OpenSans/OpenSans-Italic.woff") format("woff"); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "OpenSans"; + src: url("/public/fonts/OpenSans/OpenSans-BoldItalic.woff2") format("woff2"), + url("/public/fonts/OpenSans/OpenSans-BoldItalic.woff") format("woff"); + font-weight: bold; + font-style: italic; + font-display: swap; +} diff --git a/app/javascript/src/styles/base/_themable.scss b/app/javascript/src/styles/base/_themable.scss index 2c7dee3aa..4634774d1 100644 --- a/app/javascript/src/styles/base/_themable.scss +++ b/app/javascript/src/styles/base/_themable.scss @@ -19,3 +19,15 @@ @function themed($key) { @return var(--#{$key}); } + +@mixin with-theme($key, $value) { + body[data-th-#{$key}="#{$value}"] { + @content; + } +} + +@mixin with-setting($key, $value) { + body[data-st-#{$key}="#{$value}"] { + @content; + } +} diff --git a/app/javascript/src/styles/base/_vars.scss b/app/javascript/src/styles/base/_vars.scss index 9d48be57e..c8f7ae949 100644 --- a/app/javascript/src/styles/base/_vars.scss +++ b/app/javascript/src/styles/base/_vars.scss @@ -20,7 +20,7 @@ $dtext_h3_size: 1.6em; $dtext_h4_size: 1.4em; $dtext_h5_size: 1.2em; $dtext_h6_size: 1em; -$base_font_family: Verdana, sans-serif; +$base_font_family: Verdana, Geneva, sans-serif; $box-shadow-size: 2px 2px 5px; diff --git a/app/javascript/src/styles/common/_footer.scss b/app/javascript/src/styles/common/_footer.scss index 31c2c6935..53cf3473e 100644 --- a/app/javascript/src/styles/common/_footer.scss +++ b/app/javascript/src/styles/common/_footer.scss @@ -29,7 +29,7 @@ footer.footer-wrapper { margin-right: -3.25rem; background: themed("color-background") themed("image-background"); - border-radius: 50%; + border-radius: 25%; padding: 0.5rem; } } diff --git a/app/javascript/src/styles/common/_standard_elements.scss b/app/javascript/src/styles/common/_standard_elements.scss index 7de3c1be4..d4a9d6d5b 100644 --- a/app/javascript/src/styles/common/_standard_elements.scss +++ b/app/javascript/src/styles/common/_standard_elements.scss @@ -21,11 +21,12 @@ line-height: st-value(100); padding: st-value(100) / 2; height: st-value(100) * 2; + box-sizing: border-box; // TODO What if button is on a light background background: $button-background; color: $button-text-color; - &:hover { background: $button-background-hover; } + &:hover, &:active { background: $button-background-hover; } & > svg { // Icon should be slightly larger than text, @@ -64,3 +65,57 @@ padding: (st-value(100) / 2) 0; } } + + +// Colored buttons +.st-button.danger { + background: palette("background-red"); + &:hover, &:active { + background: palette("background-red-d5") + } +} + + + +// Toggle switch +label.st-toggle { + display: flex; + width: 2.5rem; + height: 0.75rem; + box-sizing: border-box; + position: relative; + + background-color: themed("color-foreground"); + // transition: background-color 200ms; + border-radius: 0.25rem; + cursor: pointer; + font-size: 0; + + box-shadow: inset 0 0 0.25rem #00000060; + + &::after { + content: ""; + width: 1rem; + height: 1rem; + position: absolute; + top: -0.125rem; + left: 0rem; + + background: themed("color-link"); + border-radius: 0.25rem; + transition: left 100ms, background-color 200ms; + } +} +input[type="checkbox"][disabled].st-toggle + label.st-toggle { + background: palette("background-grey"); + cursor: not-allowed; +} +input[type="checkbox"].st-toggle { display: none; } +input[type="checkbox"].st-toggle:checked + label.st-toggle { + // background-color: palette("background-green-d5"); + &::after { + content: ""; + left: 1.5rem; + background: themed("color-link-active"); + } +} 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 160d6001a..54c417d12 100644 --- a/app/javascript/src/styles/common/navigation.scss +++ b/app/javascript/src/styles/common/navigation.scss @@ -5,7 +5,7 @@ nav.navigation { grid-template-rows: min-content min-content min-content min-content auto; width: 100%; // otherwise narrow when fixed - z-index: 20; // otherwise post labels layered above + z-index: 200; // above post labels and notes position: relative; @@ -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"); } } @@ -187,6 +187,8 @@ nav.navigation { padding: 1rem 0.5rem; } li.current a { background-color: themed("color-foreground"); } + + li.nav-hidden { display: none; } } .nav-secondary { @@ -493,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"); @@ -526,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; } } } } @@ -562,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/common/notices.scss b/app/javascript/src/styles/common/notices.scss index 37ebfb812..f94727d39 100644 --- a/app/javascript/src/styles/common/notices.scss +++ b/app/javascript/src/styles/common/notices.scss @@ -16,7 +16,7 @@ div#notice { top: 1rem; left: 25%; width: 50%; - z-index: 100; + z-index: 500; color: themed("color-text"); background-color: themed("color-success"); diff --git a/app/javascript/src/styles/common/paginator.scss b/app/javascript/src/styles/common/paginator.scss index f00e19abe..84c7084e3 100644 --- a/app/javascript/src/styles/common/paginator.scss +++ b/app/javascript/src/styles/common/paginator.scss @@ -102,7 +102,7 @@ nav.pagination { // Sequential paginator -.paginator.sequential { +nav.pagination.sequential { gap: 5rem; a span { display: block; } } diff --git a/app/javascript/src/styles/common/post_search.scss b/app/javascript/src/styles/common/post_search.scss index 68ce953d4..f9655f607 100644 --- a/app/javascript/src/styles/common/post_search.scss +++ b/app/javascript/src/styles/common/post_search.scss @@ -13,7 +13,7 @@ textarea { // Override default texarea styles - font-family: Verdana, sans-serif; + font-family: $base_font_family; font-size: 1rem; line-height: 1rem; padding: 0.5rem 0 0.5rem 0.5rem; @@ -30,7 +30,7 @@ // Disable manual resizing resize: none; - border-radius: 3px 0 0 3px; + border-radius: 0.25rem 0 0 0.25rem; box-sizing: border-box; flex: 1; @@ -43,19 +43,27 @@ font-size: unset; max-width: unset; - font-size: 1rem; - line-height: 1rem; + display: flex; + align-items: center; padding: 0.5rem; - border-radius: 0 3px 3px 0; + border-radius: 0 0.25rem 0.25rem 0; background: white; + color: black; - span { display: none; } + svg { + height: 1.5rem; + width: 1.5rem; + margin: -0.25rem 0 -0.25rem; + } } @include window-larger-than(800px) { - textarea, button[type="submit"] { - font-size: 0.85rem; + textarea { font-size: 0.85rem; } + button[type="submit"] svg { + height: 1.25rem; + width: 1.25rem; + margin: -0.125rem 0 -0.125rem; } } } diff --git a/app/javascript/src/styles/common/tables.scss b/app/javascript/src/styles/common/tables.scss index e08ec9a00..6092e32fb 100644 --- a/app/javascript/src/styles/common/tables.scss +++ b/app/javascript/src/styles/common/tables.scss @@ -92,8 +92,6 @@ table.search { } table.aligned-vertical { - @extend .search; - tr { height: 1.75em; } diff --git a/app/javascript/src/styles/specific/home.scss b/app/javascript/src/styles/specific/home.scss index 6bcb8752d..d5dfcb53c 100644 --- a/app/javascript/src/styles/specific/home.scss +++ b/app/javascript/src/styles/specific/home.scss @@ -43,7 +43,7 @@ body.c-static.a-home { input[type="text"] { flex: 1; border: 0; - border-radius: 3px 0 0 3px; + border-radius: 0.25rem 0 0 0.25rem; padding: 0.5rem; font-size: 1rem; @@ -57,9 +57,14 @@ body.c-static.a-home { button[type="submit"] { background: white; - border-radius: 0 3px 3px 0; - padding: 0 0.5em; - font-size: 1rem; + border-radius: 0 0.25rem 0.25rem 0; + padding: 0 0.5rem; + + svg { + height: 1.25rem; + width: 1.25rem; + margin: -0.125rem 0 -0.125rem; + } } } diff --git a/app/javascript/src/styles/specific/post_index.scss b/app/javascript/src/styles/specific/post_index.scss deleted file mode 100644 index 751a66ef5..000000000 --- a/app/javascript/src/styles/specific/post_index.scss +++ /dev/null @@ -1,287 +0,0 @@ -body.c-posts.a-index, body.c-favorites.a-index { - #page { - // Override the theme to instead - // project it upon the content area - background: none; - padding: 0; - } - - // Exhibit A - // Makes the content area take up the - // full height of the page. Yes, really. - #page, #c-posts, #c-favorites, #a-index { - // I hate both this and myself - display: flex; - flex-flow: column; - flex: 1; - } -} - -.post-index { - display: grid; - - grid-template-areas: - "search " - "content" - "sidebar"; - grid-template-columns: 1fr; - grid-template-rows: min-content 1fr min-content; - - flex: 1; // See Exhibit A - - // 1. Searchbox - .search { - grid-area: search; - - padding: 0.5rem 0.25rem; - background-color: #152f56; - background-color: themed("color-foreground"); - box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background"); - - h1 { - font-size: $h3-size; - } - - .search-controls { - display: none; - flex-flow: column; - } - } - - // 2. Content - .content { - display: flex; // See Exhibit A - flex-flow: column; - - grid-area: content; - - // Imported from #page - padding: 0.5rem 0.25rem themed("content-padding-bottom"); - background-color: #152f56; - background-color: themed("color-foreground"); - background-image: themed("image-foreground"); - background-position: themed("image-foreground-position"); - background-repeat: themed("image-foreground-repeat"); - - // Quick tag edit - #edit-dialog textarea { - margin-bottom: 0.25rem; - } - - // Actual content area: - // posts and pagination - .post-index-gallery { - display: flex; - flex-flow: column; - gap: 1rem; - - flex: 1; // See Exhibit A - - .posts-container { - flex: 1; // See Exhibit A - grid-auto-rows: min-content; - } - } - } - - // 3. Sidebar - .sidebar { - grid-area: sidebar; - - display: flex; - flex-flow: column; - gap: 1em; - - padding: 0.5rem 0.25rem; - background-color: #152f56; - background-color: themed("color-foreground"); - box-shadow: inset 0px 0.25rem 0.25rem -0.25rem themed("color-background"); - - // Mode selection - #mode-box-mode, #mode-box #set-id { - width: 100%; - - // Match the searchbox - padding: 0.5em; - font-family: Verdana, sans-serif; - font-size: 1.05em; - } - } -} - - -// Desktop -.post-index { - @include window-larger-than(50rem) { - grid-template-areas: - "search content" - "sidebar content"; - grid-template-columns: 14rem 1fr; - grid-template-rows: min-content 1fr; - - .search { - box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background"); - margin-top: 0.25rem; - border-top-left-radius: 0.25rem; - padding: 0.5rem; - - .search-controls { - display: flex; - margin-top: 0.5rem; - } - } - - .sidebar { - box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background"); - margin-bottom: 0.25rem; - border-bottom-left-radius: 0.25rem; - padding: 0.5rem - } - - .content { - border-radius: 0.25rem; - } - } -} - - -// Fullscreen -body.c-posts.a-index[data-st-fullscreen="true"] { - // Desktop-only, for obvious reasons - @include window-larger-than(50rem) { - .post-index { - grid-template-areas: - "search " - "content"; - grid-template-columns: 1fr; - - .search { - display: flex; - - border-radius: 0.25rem 0.25rem 0 0; - box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background"); - - .post-search { - flex: 1; - } - - .search-controls { - display: flex; - justify-content: right; - align-self: end; - margin: 0 0 0 0.5rem; - - .st-button.w100 { - width: unset; - span { display: none; } - } - } - } - .sidebar { display: none; } - .content { - border-radius: 0 0 0.25rem 0.25rem; - } - } - } -} - - -// FEATURES -// Wiki Excerpt -.wiki-excerpt { - display: flex; - flex-flow: column; - position: relative; - - background: themed("color-section"); - border-radius: 0.25rem; - - &.hidden { display: none; } - - // header - h3 { - display: flex; - align-items: center; - - font-size: 1rem; - padding: 0.5rem; - - cursor: pointer; - - & > svg { - transition: transform 0.25s; - - height: 1.5rem; - width: 1.5rem; - } - - .wiki-excerpt-dismiss { margin-left: auto; } - } - - // body - .styled-dtext { - background: linear-gradient(to top, themed("color-section"), themed("color-text")); - -webkit-background-clip: text; - background-clip: text; - color: transparent; - - min-height: 0rem; - max-height: 0rem; - max-width: 50rem; - overflow: hidden; - padding: 0 0.5rem; - - transition: max-height 0.25s; - - // Disable links - pointer-events: none; - cursor: unset; - - a { - color: unset; - text-decoration: underline; - &::after { content: none; } - } - - p:last-child { margin-bottom: 0; } - } - - // wiki link - .wiki-excerpt-readmore { - display: flex; - justify-content: center; - align-items: center; - - position: absolute; - bottom: 0; - left: 0; - right: 0; - - height: 3rem; - max-width: 50rem; - box-sizing: border-box; - - // Makes the button appear in the middle of the animation - transition: visibility 0s 0.125s; - visibility: hidden; - - span { - padding: 0.5rem 1rem; - background: themed("color-section"); - border-radius: 0.25rem; - } - } - - &.open{ - .wiki-excerpt-toggle svg { transform: rotate(90deg); } - .styled-dtext { - max-height: 10rem; - min-height: 2rem; - } - .wiki-excerpt-readmore { visibility: visible; } - } - - &.loading { - h3::after, .styled-dtext { transition: none; } - } -} diff --git a/app/javascript/src/styles/specific/users.scss b/app/javascript/src/styles/specific/users.scss index 0108e4c8c..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 @@ -200,29 +100,6 @@ div#c-users { } } } - - div#a-edit { - h1 { - margin: 0.5em 0; - } - - h2 { - margin: 0.5em 0; - } - - div.input { - margin-bottom: 2em; - } - - div.input span.hint { - display: block; - max-width: 70%; - } - - .active { - color: themed("color-link-active"); - } - } } // User signup and login 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/posts/_posts.scss b/app/javascript/src/styles/views/posts/_posts.scss new file mode 100644 index 000000000..f6dcef481 --- /dev/null +++ b/app/javascript/src/styles/views/posts/_posts.scss @@ -0,0 +1,19 @@ +// Main themes +@import "index/index"; +@include window-larger-than(50rem) { + @import "index/index.desktop"; +} + +// Features +@import "index/partials/mode_menu"; +@import "index/partials/wiki_excerpt"; +@import "index/partials/stats"; + +@include window-larger-than(50rem) { + @include with-setting("fullscreen", "true") { + @import "index/partials/fullscreen"; + } + @include with-setting("ssearch", "true") { + @import "index/partials/sticky"; + } +} diff --git a/app/javascript/src/styles/views/posts/index/_index.desktop.scss b/app/javascript/src/styles/views/posts/index/_index.desktop.scss new file mode 100644 index 000000000..115ae2aef --- /dev/null +++ b/app/javascript/src/styles/views/posts/index/_index.desktop.scss @@ -0,0 +1,57 @@ +// Desktop-only +// Rollout at 50rem +.post-index { + grid-template-areas: + "search content" + "sidebar content"; + grid-template-columns: 15rem 1fr; + grid-template-rows: min-content 1fr; + + .search { + box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background"); + padding: 0.5rem 0.75rem 0.5rem 0.5rem; + + // Align the controls properly + position: relative; + + .search-controls { + display: flex; + flex-flow: row; + justify-content: right; + + position: absolute; + bottom: -1.633rem; + right: 0.5rem; + + padding: 0.25rem; + gap: 0.5rem; + + background: themed("color-foreground"); + border-radius: 0 0 0.25rem 0.25rem; + + button { + height: 1.5rem; + padding: 0; + + svg { + height: 1.25rem; + width: 1.25rem; + padding: 0.25rem; + margin: -0.125rem 0; + } + } + } + } + + .sidebar { + box-shadow: inset -0.25rem 0px 0.25rem -0.25rem themed("color-background"); + padding: 0.5rem 0.75rem 0.5rem 0.5rem; + + font-size: 100%; + } + + .content { + border-radius: 0 0.25rem 0.25rem 0; + padding: 0.5rem 0.75rem themed("content-padding-bottom"); + } +} diff --git a/app/javascript/src/styles/views/posts/index/_index.scss b/app/javascript/src/styles/views/posts/index/_index.scss new file mode 100644 index 000000000..0f24b040d --- /dev/null +++ b/app/javascript/src/styles/views/posts/index/_index.scss @@ -0,0 +1,108 @@ +body.c-posts.a-index, body.c-favorites.a-index { + #page { + // Override the theme to instead + // project it upon the content area + background: var(--color-foreground); + padding: 0; + } + + // Exhibit A + // Makes the content area take up the + // full height of the page. Yes, really. + #page, #c-posts, #c-favorites, #a-index { + // I hate both this and myself + display: flex; + flex-flow: column; + flex: 1; + } +} + + +// Post gallery +.post-index { + display: grid; + + grid-template-areas: + "search " + "content" + "sidebar"; + grid-template-columns: 1fr; + grid-template-rows: min-content 1fr min-content; + + flex: 1; // See Exhibit A + + // 1. Searchbox + .search { + grid-area: search; + + padding: 0.5rem 0.25rem; + box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background"); + + h1 { + font-size: $h3-size; + } + + .search-controls { display: none; } + } + + // 2. Content + .content { + display: flex; // See Exhibit A + flex-flow: column; + + grid-area: content; + + // Imported from #page + padding: 0.5rem 0.25rem themed("content-padding-bottom"); + background-color: #152f56; + background-color: themed("color-foreground"); + background-image: themed("image-foreground"); + background-position: themed("image-foreground-position"); + background-repeat: themed("image-foreground-repeat"); + + // Quick tag edit + #edit-dialog textarea { + margin-bottom: 0.25rem; + } + + // Actual content area: + // posts and pagination + .post-index-gallery { + display: flex; + flex-flow: column; + gap: 1rem; + + flex: 1; // See Exhibit A + + .posts-container { + flex: 1; // See Exhibit A + grid-auto-rows: min-content; + } + } + } + + // 3. Sidebar + .sidebar { + grid-area: sidebar; + + display: flex; + flex-flow: column; + gap: 1em; + + padding: 0.5rem 0.25rem; + box-shadow: inset 0px 0.25rem 0.25rem -0.25rem themed("color-background"); + + // By popular demand + font-size: 150%; + + // Mode selection + #mode-box-mode, #mode-box #set-id { + width: 100%; + + // Match the searchbox + padding: 0.5em; + font-family: $base_font_family; + font-size: 1.05em; + } + } +} diff --git a/app/javascript/src/styles/views/posts/index/partials/_fullscreen.scss b/app/javascript/src/styles/views/posts/index/partials/_fullscreen.scss new file mode 100644 index 000000000..3d3279d5c --- /dev/null +++ b/app/javascript/src/styles/views/posts/index/partials/_fullscreen.scss @@ -0,0 +1,35 @@ +// Fullscreen mode +// Only relevant on desktop +.post-index { + grid-template-areas: + "search " + "content"; + grid-template-columns: 1fr; + + .search { + display: flex; + border-radius: 0.25rem 0.25rem 0 0; + box-shadow: inset 0px -0.25rem 0.25rem -0.25rem themed("color-background"); + + margin: 0.25rem 0 0; + padding: 0.5rem; + + flex-wrap: wrap; + justify-content: right; + z-index: 11; // above posts and labels + + .post-search { width: 100%; } + + #search-fullscreen { + background: themed("color-button-active"); + color: black; + } + + .search-controls { right: 0.25rem; } + } + .sidebar { display: none; } + .content { + border-radius: 0 0 0.25rem 0.25rem; + .posts-index-stats { margin-right: 5rem; } + } +} diff --git a/app/javascript/src/styles/specific/post_mode_menu.scss b/app/javascript/src/styles/views/posts/index/partials/_mode_menu.scss similarity index 99% rename from app/javascript/src/styles/specific/post_mode_menu.scss rename to app/javascript/src/styles/views/posts/index/partials/_mode_menu.scss index 467f43954..2bf3f62b8 100644 --- a/app/javascript/src/styles/specific/post_mode_menu.scss +++ b/app/javascript/src/styles/views/posts/index/partials/_mode_menu.scss @@ -32,4 +32,4 @@ $modes: ( input { flex: 1; } button { padding: 0 0.25em; } -} \ No newline at end of file +} diff --git a/app/javascript/src/styles/views/posts/index/partials/_stats.scss b/app/javascript/src/styles/views/posts/index/partials/_stats.scss new file mode 100644 index 000000000..5f7b55196 --- /dev/null +++ b/app/javascript/src/styles/views/posts/index/partials/_stats.scss @@ -0,0 +1,13 @@ +.posts-index-stats { + display: flex; + justify-content: end; + + margin: -0.5rem 0rem 0.25rem; + + font-size: 0.75rem; + line-height: 0.75rem; + color: #fffa; + font-family: monospace; + + & > span { cursor: help; } +} diff --git a/app/javascript/src/styles/views/posts/index/partials/_sticky.scss b/app/javascript/src/styles/views/posts/index/partials/_sticky.scss new file mode 100644 index 000000000..45514b76c --- /dev/null +++ b/app/javascript/src/styles/views/posts/index/partials/_sticky.scss @@ -0,0 +1,19 @@ +.post-index .search { + position: sticky; + top: 0; + + background: themed("color-foreground"); + border-radius: 0.25rem; + + // on top of thumbnail labels + z-index: 11; + + #search-sticky { + background: themed("color-button-active"); + color: black; + } +} + +&[data-th-sheader="true"] { + .post-index .search { top: 3.75rem; } +} diff --git a/app/javascript/src/styles/views/posts/index/partials/_wiki_excerpt.scss b/app/javascript/src/styles/views/posts/index/partials/_wiki_excerpt.scss new file mode 100644 index 000000000..4beb26345 --- /dev/null +++ b/app/javascript/src/styles/views/posts/index/partials/_wiki_excerpt.scss @@ -0,0 +1,101 @@ +.wiki-excerpt { + display: flex; + flex-flow: column; + position: relative; + + background: themed("color-section"); + border-radius: 0.25rem; + + &.hidden { display: none; } + + // header + h3 { + display: flex; + align-items: center; + + font-size: 1rem; + padding: 0.5rem; + + cursor: pointer; + + & > svg { + transition: transform 0.25s; + + height: 1.5rem; + width: 1.5rem; + } + + .wiki-excerpt-dismiss { margin-left: auto; } + } + + // body + .styled-dtext { + background: linear-gradient(to top, themed("color-section"), themed("color-text")); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + + min-height: 0rem; + max-height: 0rem; + max-width: 50rem; + overflow: hidden; + padding: 0 0.5rem; + + transition: max-height 0.25s; + + // Disable links + pointer-events: none; + cursor: unset; + + a { + color: unset; + text-decoration: underline; + &::after { content: none; } + } + + // Quote visual bug + blockquote { background: unset; } + + // Remove offset caused by paragraphs + p:last-child { margin-bottom: 0; } + } + + // wiki link + .wiki-excerpt-readmore { + display: flex; + justify-content: center; + align-items: center; + + position: absolute; + bottom: 0; + left: 0; + right: 0; + + height: 3rem; + max-width: 50rem; + box-sizing: border-box; + + // Makes the button appear in the middle of the animation + transition: visibility 0s 0.125s; + visibility: hidden; + + span { + padding: 0.5rem 1rem; + background: themed("color-section"); + border-radius: 0.25rem; + } + } + + &.open{ + .wiki-excerpt-toggle svg { transform: rotate(90deg); } + .styled-dtext { + max-height: 10rem; + min-height: 2rem; + } + .wiki-excerpt-readmore { visibility: visible; } + } + + &.loading { + h3::after, .styled-dtext { transition: none; } + } +} diff --git a/app/javascript/src/styles/views/users/_users.scss b/app/javascript/src/styles/views/users/_users.scss new file mode 100644 index 000000000..46f889bd9 --- /dev/null +++ b/app/javascript/src/styles/views/users/_users.scss @@ -0,0 +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 new file mode 100644 index 000000000..785c99a0b --- /dev/null +++ b/app/javascript/src/styles/views/users/edit/_edit.scss @@ -0,0 +1,200 @@ +body.c-users.a-edit { + + form.simple_form { + background: unset; + padding: unset; + margin: unset; + + input[type="text"], input[type="number"], textarea, select { + width: 100%; + max-width: unset; + box-sizing: border-box; + border-radius: 0.25rem; + font-size: 1rem; + padding: 0.25rem; + } + } + + #settings-account-buttons tab-body { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + + .st-button { + white-space: nowrap; + justify-content: center; + } + } + + tab-submit { + input[type="submit"] { + background: themed("color-tag-artist"); + font-size: 1.25rem; + padding: 0.25rem 1rem; + margin: 0.5rem; + border-radius: 0.25rem; + + &:hover, &:active { background: themed("color-tag-artist-alt"); } + } + } +} + + + + +// Tabs section +tabs-menu { + margin-top: 0.5rem; + display: flex; + width: 100%; + max-width: 50rem; + flex-wrap: wrap; + + // TODO Smooth drag when overflowing + overflow: auto; + + & > button { + background: unset; + color: themed("color-link"); + padding: 0.5rem 1rem; + border-bottom: 2px solid themed("color-section"); + font-size: 1rem; + line-height: 1rem; + + &.active { + background: themed("color-section"); + border-radius: 0.25rem 0.25rem 0 0; + &:first-child { + border-bottom-left-radius: 0.25rem; + } + } + } + + span.spacer { + flex: 1; + border-bottom: 2px solid themed("color-section"); + } + + input[name="search"] { + border-radius: 0 0 0.25rem 0.25rem; + font-size: 1rem; + line-height: 1rem; + padding: 0.25rem; + width: 100%; + box-sizing: border-box; + } +} + + +// Tabs structure +tabs-content { + display: flex; + flex-flow: column; + max-width: 50rem; + + tab-group { + display: none; + &.active { display: flex; } + + font-size: 1rem; + line-height: 1rem; + padding: 0.5rem 0.75rem 0.5rem; + margin: 0.5rem 0 0; + + background: themed("color-section-lighten-5"); + border-radius: 0.25rem 0.25rem 0 0; + } + + tab-entry { + display: none !important; + &.active { display: grid !important; } + &.flex.active { display: flex !important; } + grid-template-areas: "head" "body" "hint"; + grid-template-columns: 1fr; + + align-items: center; + gap: 0.25rem; + + background: themed("color-section"); + padding: 0.5rem; + + tab-head { + grid-area: head; + margin-left: 0.25em; + font-size: 0.85rem; + + label { font-weight: normal; } + } + + tab-body { + grid-area: body; + } + + tab-hint { + grid-area: hint; + + font-size: 90%; + color: themed("color-text-muted"); + } + + &.inline { + grid-template-areas: "head body" "hint hint"; + grid-template-columns: 2fr 1fr; + + tab-body { justify-self: end; } + } + + &.blocky { + grid-template-areas: "body"; + grid-template-columns: 1fr; + } + + &.bigtext { + grid-template-columns: 1fr; + grid-template-areas: "head" "body" "hint"; + } + + &.buttony tab-body { + display: grid; + grid-template-columns: 1fr 4rem; + + input[type="text"] { border-radius: 0.25rem 0 0 0.25rem !important; } + input[disabled] { cursor: not-allowed; } + a.st-button { + border-radius: 0 0.25rem 0.25rem 0 !important; + justify-content: center; + } + } + } +} + +tab-submit { + position: sticky; + bottom: 0; + + background: themed("color-section"); + border-radius: 0.25rem; + width: min-content; + margin-top: 0.5rem; +} + +@include window-larger-than(50rem) { + tabs-menu { + input[name="search"] { + width: unset; + border-radius: 0.25rem 0.25rem 0.25rem 0; + border-bottom: 2px solid themed("color-section"); + } + } + + tabs-content { + tab-entry { + grid-template-areas: "head body" ". hint"; + grid-template-columns: 1fr 1fr; + &.inline { + grid-template-areas: "head hint body"; + grid-template-columns: 8fr 7fr 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/artist.rb b/app/models/artist.rb index 4d142a84b..2f9c3ad6c 100644 --- a/app/models/artist.rb +++ b/app/models/artist.rb @@ -487,6 +487,8 @@ class Artist < ApplicationRecord if params[:is_linked].to_s.truthy? q = q.where("linked_user_id IS NOT NULL") + elsif params[:is_linked].to_s.falsy? + q = q.where("linked_user_id IS NULL") end case params[:order] diff --git a/app/models/tag.rb b/app/models/tag.rb index 62db4a89c..fc304d381 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -80,10 +80,14 @@ class Tag < ApplicationRecord def category_for(tag_name) Cache.fetch("tc:#{tag_name}") do - Tag.where(name: tag_name).pick(:category).to_i + category_for!(tag_name).to_i end end + def category_for!(tag_name) + Tag.where(name: tag_name).pick(:category) + end + def categories_for(tag_names, disable_cache: false) if disable_cache tag_cats = {} 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/models/wiki_page.rb b/app/models/wiki_page.rb index a0ae4c1cd..a5c6d551e 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true class WikiPage < ApplicationRecord - class RevertError < Exception ; end + class RevertError < Exception; end before_validation :normalize_title before_validation :normalize_other_names before_validation :normalize_parent + before_save :log_changes + before_save :update_tag, if: :tag_changed? + before_destroy :validate_not_used_as_help_page + before_destroy :log_destroy after_save :create_version + after_save :update_help_page, if: :saved_change_to_title? + normalizes :body, with: ->(body) { body.gsub("\r\n", "\n") } - validates :title, uniqueness: { :case_sensitive => false } + + validates :title, uniqueness: { case_sensitive: false } validates :title, presence: true validates :title, tag_name: true, if: :title_changed? validates :body, presence: { unless: -> { is_deleted? || other_names.present? || parent.present? } } @@ -19,12 +26,8 @@ class WikiPage < ApplicationRecord validate :validate_redirect validate :validate_not_locked - before_destroy :validate_not_used_as_help_page - before_destroy :log_destroy - before_save :log_changes - after_save :update_help_page, if: :saved_change_to_title? - attr_accessor :skip_secondary_validations, :edit_reason + array_attribute :other_names belongs_to_creator belongs_to_updater @@ -33,19 +36,19 @@ class WikiPage < ApplicationRecord has_many :versions, -> { order("wiki_page_versions.id ASC") }, class_name: "WikiPageVersion", dependent: :destroy has_one :help_page, foreign_key: "wiki_page", primary_key: "title" - def log_destroy - ModAction.log(:wiki_page_delete, {wiki_page: title, wiki_page_id: id}) - end - def log_changes if title_changed? && !new_record? - ModAction.log(:wiki_page_rename, {new_title: title, old_title: title_was}) + ModAction.log(:wiki_page_rename, { new_title: title, old_title: title_was }) end if is_locked_changed? - ModAction.log(is_locked ? :wiki_page_lock : :wiki_page_unlock, {wiki_page: title}) + ModAction.log(is_locked ? :wiki_page_lock : :wiki_page_unlock, { wiki_page: title }) end end + def log_destroy + ModAction.log(:wiki_page_delete, { wiki_page: title, wiki_page_id: id }) + end + module SearchMethods def titled(title) find_by(title: WikiPage.normalize_name(title)) @@ -123,7 +126,7 @@ class WikiPage < ApplicationRecord module ApiMethods def method_attributes - super + [:creator_name, :category_id] + super + %i[creator_name category_id] end end @@ -140,9 +143,92 @@ class WikiPage < ApplicationRecord end end + module TagMethods + def tag + @tag ||= super + end + + def category_id + return @category_id if instance_variable_defined?(:@category_id) + @category_id = tag&.category + end + + def category_id=(value) + return if value.blank? || value.to_i == category_id + category_id_will_change! + @category_id = value.to_i + end + + def category_is_locked + return @category_is_locked if instance_variable_defined?(:@category_is_locked) + @category_is_locked = tag&.is_locked || false + end + + def category_is_locked=(value) + return if value == category_is_locked + category_is_locked_will_change! + @category_is_locked = value + end + + def category_id_changed? + attribute_changed?("category_id") + end + + def category_id_will_change! + attribute_will_change!("category_id") + end + + def category_is_locked_changed? + attribute_changed?("category_is_locked") + end + + def category_is_locked_will_change! + attribute_will_change!("category_is_locked") + end + + def tag_update_map + {}.tap do |updates| + updates[:category] = @category_id if category_id_changed? + updates[:is_locked] = @category_is_locked if category_is_locked_changed? + end + end + + def tag_changed? + tag_update_map.present? + end + + def update_tag + updates = tag_update_map + @tag = Tag.find_or_create_by_name(title) + + return if updates.empty? + unless @tag.category_editable_by?(CurrentUser.user) + reload_tag_attributes + errors.add(:category_id, "Cannot be changed") + throw(:abort) + end + + @tag.update(updates) + @tag.save + + if @tag.invalid? + errors.add(:category_id, @tag.errors.full_messages.join(", ")) + throw(:abort) + end + + reload_tag_attributes + end + + def reload_tag_attributes + remove_instance_variable(:@category_id) if instance_variable_defined?(:@category_id) + remove_instance_variable(:@category_is_locked) if instance_variable_defined?(:@category_is_locked) + end + end + extend SearchMethods include ApiMethods include HelpPageMethods + include TagMethods def user_not_limited allowed = CurrentUser.can_wiki_edit_with_reason @@ -156,7 +242,7 @@ class WikiPage < ApplicationRecord def validate_not_locked if is_locked? && !CurrentUser.is_janitor? errors.add(:is_locked, "and cannot be updated") - return false + false end end @@ -203,7 +289,12 @@ class WikiPage < ApplicationRecord end def normalize_title - self.title = title.downcase.tr(" ", "_") + title = self.title.downcase.tr(" ", "_") + if title =~ /\A(#{Tag.categories.regexp}):(.+)\Z/ + self.category_id = Tag.categories.value_for($1) + title = $2 + end + self.title = title end def normalize_other_names @@ -226,16 +317,12 @@ class WikiPage < ApplicationRecord @skip_secondary_validations = value.to_s.truthy? end - def category_id - Tag.category_for(title) - end - def pretty_title - title&.tr("_", " ") || '' + title&.tr("_", " ") || "" end def pretty_title_with_category - return pretty_title if category_id == 0 + return pretty_title if category_id.blank? || category_id == 0 "#{Tag.category_for_value(category_id)}: #{pretty_title}" end @@ -274,10 +361,6 @@ class WikiPage < ApplicationRecord else match end - end.map {|x| x.downcase.tr(" ", "_").to_s}.uniq - end - - def visible? - true + end.map { |x| x.downcase.tr(" ", "_").to_s }.uniq end end diff --git a/app/models/wiki_page_version.rb b/app/models/wiki_page_version.rb index e0e42536e..39830e612 100644 --- a/app/models/wiki_page_version.rb +++ b/app/models/wiki_page_version.rb @@ -6,7 +6,6 @@ class WikiPageVersion < ApplicationRecord belongs_to_updater user_status_counter :wiki_edit_count, foreign_key: :updater_id belongs_to :artist, optional: true - delegate :visible?, to: :wiki_page module SearchMethods def for_user(user_id) 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/artists/_search.html.erb b/app/views/artists/_search.html.erb index bb239b0d7..ae7e13233 100644 --- a/app/views/artists/_search.html.erb +++ b/app/views/artists/_search.html.erb @@ -3,6 +3,6 @@ <%= f.input :url_matches, label: "URL", as: :string %> <%= f.user :creator %> <%= f.input :has_tag, label: "Has tag?", collection: [["Yes", true], ["No", false]], include_blank: true %> - <%= f.input :is_linked, label: "Linked Only", as: :boolean %> + <%= f.input :is_linked, label: "Linked?", collection: [["Yes", true], ["No", false]], include_blank: true %> <%= f.input :order, collection: [["Recently created", "created_at"], ["Last updated", "updated_at"], ["Name", "name"], ["Post count", "post_count"]] %> <% end %> diff --git a/app/views/layouts/_main_links.html.erb b/app/views/layouts/_main_links.html.erb index 9481f0869..47e689677 100644 --- a/app/views/layouts/_main_links.html.erb +++ b/app/views/layouts/_main_links.html.erb @@ -1,3 +1,9 @@ +<% if CurrentUser.is_anonymous? %> + <%= decorated_nav_link_to("Log In", :log_in, new_session_path, class: "nav-hidden") %> +<% else %> + <%= decorated_nav_link_to("Profile", :user, user_path(CurrentUser.user), class: "nav-hidden") %> +<% end %> +<%= decorated_nav_link_to("Artists", :brush, artists_path) %> <%= decorated_nav_link_to("Posts", :images, posts_path) %> <%= decorated_nav_link_to("Pools", :library, gallery_pools_path) %> <%= decorated_nav_link_to("Sets", :group, post_sets_path) %> diff --git a/app/views/layouts/_nav.html.erb b/app/views/layouts/_nav.html.erb index a8f44f2da..7d58b50ee 100644 --- a/app/views/layouts/_nav.html.erb +++ b/app/views/layouts/_nav.html.erb @@ -20,11 +20,11 @@ <% if CurrentUser.is_anonymous? %> -
-
-

Settings

+
+

Settings

- <%= custom_form_for @user do |f| %> -

- <%= link_to "Basic", "#basic-settings", :class => "active" %> - | <%= link_to "Advanced", "#advanced-settings" %> - <% if CurrentUser.user.id == @user.id %> - | <%= link_to "Change password", edit_user_password_path(:user_id => @user.id), :id => "change-password" %> - | <%= link_to "Delete account", maintenance_user_deletion_path, :id => "delete-account" %> - <% end %> -

+ + + + + + + -
-
- + <%= custom_form_for @user do |form| %> + + <%= render "users/partials/edit/basic", form: form %> + <%= render "users/partials/edit/advanced", form: form %> + <%= render "users/partials/edit/blacklist", form: form %> + + <%= form.button :submit, "Save Settings" %> + + + <% end %> +
-

<%= link_to "Request a name change", new_user_name_change_request_path %>

-
- -
- -

- <% if CurrentUser.user.email.present? %> - <%= CurrentUser.user.email %> - <% else %> - blank - <% end %> - – - <%= link_to "Change your email", new_maintenance_user_email_change_path %> -

-
- - <%= f.input :avatar_id, as: :string, label: "Avatar Post ID" %> - - <%= f.input :profile_about, as: :dtext, label: "About Me", limit: Danbooru.config.user_about_max_size, allow_color: true %> - - <%= f.input :profile_artinfo, as: :dtext, label: "Commission Info", limit: Danbooru.config.user_about_max_size, allow_color: true %> - - <%= f.input :time_zone, :include_blank => false %> - - <%= f.input :receive_email_notifications, :as => :select, :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %> - - <%= f.input :comment_threshold, :hint => "Comments below this score will be hidden by default." %> - - <%= f.input :default_image_size, :hint => "Show original image size, scaled to fit, scaled to fit vertically, or show resized #{Danbooru.config.large_image_width} pixel sample version.", :label => "Default image width", :collection => [["Original", "original"], ["Fit (Horizontal)", "fit"], ["Fit (Vertical)", "fitv"], ["Sample (#{Danbooru.config.large_image_width}px)", "large"]], :include_blank => false %> - - <%= f.input :per_page, :label => "Posts per page", :as => :select, :collection => (25..250), :include_blank => false %> - - <%= f.input :enable_safe_mode, :label => "Safe mode", :hint => "Show only safe images. Hide questionable and explicit images.", :as => :select, :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %> - - <%= f.input :blacklisted_tags, :hint => "Put any tag combinations you never want to see here. Each combination should go on a separate line.
View help.".html_safe, autocomplete: "tag-query", input_html: { size: "40x5" } %> - - <%= f.input :blacklist_users, hint: "Hide comments, blips and forum posts from users that have been blacklisted, in addition to posts.", as: :select, include_blank: false, collection: [["Yes", "true"], ["No", "false"]] %> - - -
- <%= f.input :style_usernames, :as => :select, :label => "Colored usernames", - :hint => raw("Color each user's name depending on their level. See #{link_to 'the legend', wiki_page_path(id: 'e621:colored_usernames')} for what the colors are."), - :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %> - - <%= f.input :enable_keyboard_navigation, :as => :select, :include_blank => false, :label => "Enable keyboard shortcuts", :collection => [["Yes", "true"], ["No", "false"]], - hint: raw("Enables the use of keyboard shortcuts for a majority of site actions related to posts. A list of keyboard shortcuts is available #{link_to 'here', keyboard_shortcuts_path}.")%> - - <%= f.input :enable_auto_complete, :as => :select, :collection => [["Yes", "true"], ["No", "false"]], :include_blank => false, - hint: "Enables auto-completion on most tag and user entry fields." %> - - <%= f.input :enable_privacy_mode, :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false, - hint: "Prevent showing your favorites to others users (except staff)." %> - - <%= f.input :show_post_statistics, as: :select, collection: [["No", "false"],["Yes", "true"]], include_blank: false, - hint: "Show post statistics below posts on search pages." %> - - <%= f.input :description_collapsed_initially, as: :select, collection: [["No", "false"],["Yes", "true"]], include_blank: false, - hint: "Don't expand post descriptions on page load." %> - - <%= f.input :hide_comments, as: :select, collection: [["No", "false"],["Yes", "true"]], include_blank: false, - hint: "Do not show the comments section on post pages." %> - - <% unless CurrentUser.is_janitor? %> - <%= f.input :disable_user_dmails, label: "Disable DMails", hint: "Prevent other users from sending you DMails. You will be prevented from sending DMails to non-staff members while this option is enabled. Staff are always allowed to send you DMails.", - :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> - <% end %> - - <%= f.input :disable_cropped_thumbnails, :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false, - hint: "Disables displaying cropped thumbnails on the mobile layout of the site in favor of scaled thumbnails. Has no effect on the desktop site." %> - - <%= f.input :show_hidden_comments, label: "Show Own Hidden Comments", :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false, - hint: "Show your own hidden comments on comment pages." %> - - <% if @user.post_upload_count >= 10 %> - <%= f.input :enable_compact_uploader, label: "Enable Compact Uploader", as: :select, - collection: [["No", "false"], ["Yes", "true"]], include_blank: false, - hint: "Enables a more compact and less guided post uploader." %> - <% end %> - -
- - <%= hidden_field_tag "user[dmail_filter_attributes][id]", @user.dmail_filter.try(:id) %> - <%= text_field_tag "user[dmail_filter_attributes][words]", @user.dmail_filter.try(:words), :id => "user_dmail_filter_attributes_words", :class => "text optional", :size => 40 %> - A list of banned words (space delimited). Any dmail you receive with a banned word will automatically be deleted. -
- - <%= f.input :favorite_tags, :label => "Frequent tags", :hint => "A list of tags that you use often. They will appear when using the list of Related Tags.", autocomplete: "tag-query", input_html: { rows: 5 } %> - - <%= f.input :disable_responsive_mode, :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false, :hint => "Disable alternative layout for mobile and tablet." %> - - <%= f.input :custom_style, :label => "Custom CSS style".html_safe, :hint => "Style to apply to the whole site.", :input_html => {:size => "40x5"} %> -
- - <%= f.button :submit, "Submit" %> - <% end %> -
-
+ <% content_for(:page_title) do %> Settings <% end %> -<% content_for(:html_header) do %> - <%= javascript_tag nonce: true do -%> - $(function() { - $("#advanced-settings-section").hide(); - $("#edit-options a:not(#delete-account):not(#change-password)").on("click", function(e) { - var $target = $(e.target); - $("h2 a").removeClass("active"); - $("#basic-settings-section,#advanced-settings-section").hide(); - $target.addClass("active"); - $($target.attr("href") + "-section").show(); - e.preventDefault(); - }); - }); - <% end -%> -<% end %> - <%= render "secondary_links" %> diff --git a/app/views/users/partials/edit/_advanced.html.erb b/app/views/users/partials/edit/_advanced.html.erb new file mode 100644 index 000000000..98f4eba42 --- /dev/null +++ b/app/views/users/partials/edit/_advanced.html.erb @@ -0,0 +1,166 @@ +<% tab_name = "advanced" %> + + +Accessibility + + + Enable Keyboard Shortcuts + + <%= form.input_field :enable_keyboard_navigation, as: :boolean, class: "st-toggle" %> + <%= form.label :enable_keyboard_navigation, "!", class: "st-toggle" %> + + + The full list of shortcuts is available <%= link_to "here", keyboard_shortcuts_path %>. + + + + + Enable Auto Complete + + <%= form.input_field :enable_auto_complete, as: :boolean, class: "st-toggle" %> + <%= form.label :enable_auto_complete, "!", class: "st-toggle" %> + + + Tag and user name suggestions. + + + + + Colored Usernames + + <%= form.input_field :style_usernames, as: :boolean, class: "st-toggle" %> + <%= form.label :style_usernames, "!", class: "st-toggle" %> + + + Color names depending on the user's level. + + + + + +Privacy & Messaging + + + Hide Favorites + + <%= form.input_field :enable_privacy_mode, as: :boolean, class: "st-toggle" %> + <%= form.label :enable_privacy_mode, "!", class: "st-toggle" %> + + + Prevent your favorites from being publicly visible. + + + + + Disable DMails + + <%= form.input_field :disable_user_dmails, as: :boolean, class: "st-toggle", disabled: CurrentUser.is_staff? %> + <%= form.label :disable_user_dmails, "!", class: "st-toggle" %> + + + Prevent other users from sending you DMails. + <% if CurrentUser.is_staff? %> +
Staff members are not allowed to disable DMails. + <% end %> +
+
+ + + DMail Filters + + <%= hidden_field_tag "user[dmail_filter_attributes][id]", @user.dmail_filter.try(:id) %> + <%= text_field_tag "user[dmail_filter_attributes][words]", @user.dmail_filter.try(:words), id: "user_dmail_filter_attributes_words", class: "text optional", size: 40 %> + + + A list of banned words (space delimited). Any dmail you receive with a banned word will automatically be deleted. + + + + + +Posts & Tags + + + Show Statistics + + <%= form.input_field :show_post_statistics, as: :boolean, class: "st-toggle" %> + <%= form.label :show_post_statistics, "!", class: "st-toggle" %> + + + Show post stats below posts on search pages. + + + + + Collapse Descriptions + + <%= form.input_field :description_collapsed_initially, as: :boolean, class: "st-toggle" %> + <%= form.label :description_collapsed_initially, "!", class: "st-toggle" %> + + + Do not expand post descriptions on page load. + + + + + Frequent Tags + + <%= form.input_field :favorite_tags, label: false, autocomplete: "tag-query", rows: 5 %> + + + A list of tags that you use often.
They will appear when using the list of Related Tags. +
+
+ +<% if @user.post_upload_count >= 10 %> + + Compact Uploader + + <%= form.input_field :enable_compact_uploader, as: :boolean, class: "st-toggle" %> + <%= form.label :enable_compact_uploader, "!", class: "st-toggle" %> + + + A more compact and less guided post uploader. + + +<% end %> + + + +Mobile / Tablet + + + Disable Responsive Mode + + <%= form.input_field :disable_responsive_mode, as: :boolean, class: "st-toggle" %> + <%= form.label :disable_responsive_mode, "!", class: "st-toggle" %> + + + Disable alternative layout for mobile and tablet. + + + + + Disable Cropped Thumbnails + + <%= form.input_field :disable_cropped_thumbnails, as: :boolean, class: "st-toggle" %> + <%= form.label :disable_cropped_thumbnails, "!", class: "st-toggle" %> + + + No effect on the desktop site. + + + + + +Customization + + + Custom CSS + + <%= form.input_field :custom_style, label: false, rows: 8 %> + + + Apply CSS Styles to the whole website. + + diff --git a/app/views/users/partials/edit/_basic.html.erb b/app/views/users/partials/edit/_basic.html.erb new file mode 100644 index 000000000..d1af69828 --- /dev/null +++ b/app/views/users/partials/edit/_basic.html.erb @@ -0,0 +1,155 @@ +<% tab_name = "basic" %> + + +Account + + + Username + + <%= link_to "Edit", new_user_name_change_request_path, class: "st-button" %> + + + + + Email + + " disabled><%= link_to "Edit", new_maintenance_user_email_change_path, class: "st-button" %> + + + + + + + <%= link_to "Change password", edit_user_password_path(user_id: @user.id), class: "st-button" %> + <%= link_to "Delete account", maintenance_user_deletion_path, class: "st-button danger" %> + + + + + Time Zone + + <%= form.input_field :time_zone, + label: false, + include_blank: false + %> + + + + + Email Notifications + + <%= form.input_field :receive_email_notifications, as: :boolean, class: "st-toggle" %> + <%= form.label :receive_email_notifications, "!", class: "st-toggle" %> + + + + + + + + +Profile + + + <%= form.label :avatar_id, "Avatar Post ID" %> + + <%= form.input_field :avatar_id, as: :string, label: false %> + + + The image with this ID will be set as your avatar. + + + + + <%= form.label :profile_about, "About Me" %> + + <%= form.input_field :profile_about, + as: :dtext, + label: false, + rows: 8, + limit: Danbooru.config.user_about_max_size, + allow_color: true + %> + + + + + <%= form.label :profile_artinfo, "Commission Info" %> + + <%= form.input_field :profile_artinfo, + as: :dtext, + label: false, + rows: 8, + limit: Danbooru.config.user_about_max_size, + allow_color: true + %> + + + + + +Posts + + + <%= form.label :default_image_size, "Default image width" %> + + <%= form.input_field :default_image_size, + label: false, + collection: [["Original", "original"], ["Fit (Horizontal)", "fit"], ["Fit (Vertical)", "fitv"], ["Sample (#{Danbooru.config.large_image_width}px)", "large"]], + include_blank: false + %> + + + Show original image size, scaled to fit, scaled to fit vertically, or show resized 850 pixel sample version. + + + + + <%= form.label :per_page, "Posts per page" %> + + <%= form.input_field :per_page, label: false, as: :select, collection: (25..250).step(25), include_blank: false %> + + + + + <%= form.label :comment_threshold %> + + <%= form.input_field :comment_threshold, label: false %> + + + Comments below this score will be hidden by default. + + + + + <%= form.label :hide_comments %> + + <%= form.input_field :hide_comments, as: :boolean, class: "st-toggle" %> + <%= form.label :hide_comments, "!", class: "st-toggle" %> + + + Do not show the comments section on post pages. + + + + + <%= form.label :show_hidden_comments, "Show own hidden comments" %> + + <%= form.input_field :show_hidden_comments, as: :boolean, class: "st-toggle" %> + <%= form.label :show_hidden_comments, "!", class: "st-toggle" %> + + + Show your hidden comments on comment pages. + + + + + <%= form.label :enable_safe_mode, "Safe mode" %> + + <%= form.input_field :enable_safe_mode, as: :boolean, class: "st-toggle" %> + <%= form.label :enable_safe_mode, "!", class: "st-toggle" %> + + + Only show images rated safe. + + diff --git a/app/views/users/partials/edit/_blacklist.html.erb b/app/views/users/partials/edit/_blacklist.html.erb new file mode 100644 index 000000000..388591267 --- /dev/null +++ b/app/views/users/partials/edit/_blacklist.html.erb @@ -0,0 +1,25 @@ +<% tab_name = "blacklist" %> + + +Blacklist + + + Blacklisted Tags + + <%= form.input_field :blacklisted_tags, label: false, autocomplete: "tag-query", rows: 8 %> + + + Put any tag combinations you never want to see here. Each combination should go on a separate line. View help. + + + + + Blacklist Users + + <%= form.input_field :blacklist_users, as: :boolean, class: "st-toggle" %> + <%= form.label :blacklist_users, "!", class: "st-toggle" %> + + + Hide comments, blips and forum posts from users that have been blacklisted, in addition to posts. + + 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" %>