diff --git a/app/assets/javascripts/discourse/app/components/about-page.gjs b/app/assets/javascripts/discourse/app/components/about-page.gjs new file mode 100644 index 00000000000..a6643f18535 --- /dev/null +++ b/app/assets/javascripts/discourse/app/components/about-page.gjs @@ -0,0 +1,102 @@ +import Component from "@glimmer/component"; +import { hash } from "@ember/helper"; +import { htmlSafe } from "@ember/template"; +import PluginOutlet from "discourse/components/plugin-outlet"; +import dIcon from "discourse-common/helpers/d-icon"; +import i18n from "discourse-common/helpers/i18n"; +import escape from "discourse-common/lib/escape"; +import I18n from "discourse-i18n"; + +export default class AboutPage extends Component { + get moderatorsCount() { + return this.args.model.moderators.length; + } + + get adminsCount() { + return this.args.model.admins.length; + } + + get stats() { + return [ + { + class: "members", + icon: "users", + text: I18n.t("about.member_count", { + count: this.args.model.stats.users_count, + formatted_number: I18n.toNumber(this.args.model.stats.users_count, { + precision: 0, + }), + }), + }, + { + class: "admins", + icon: "shield-alt", + text: I18n.t("about.admin_count", { + count: this.adminsCount, + formatted_number: I18n.toNumber(this.adminsCount, { precision: 0 }), + }), + }, + { + class: "moderators", + icon: "shield-alt", + text: I18n.t("about.moderator_count", { + count: this.moderatorsCount, + formatted_number: I18n.toNumber(this.moderatorsCount, { + precision: 0, + }), + }), + }, + ]; + } + + get contactInfo() { + const url = escape(this.args.model.contact_url || ""); + const email = escape(this.args.model.contact_email || ""); + + if (url) { + return I18n.t("about.contact_info", { + contact_info: `<a href='${url}' target='_blank'>${url}</a>`, + }); + } else if (email) { + return I18n.t("about.contact_info", { + contact_info: `<a href="mailto:${email}">${email}</a>`, + }); + } else { + return null; + } + } + + <template> + <section class="about__header"> + <img class="about__banner" src={{@model.banner_image}} /> + <h3>{{@model.title}}</h3> + <p class="short-description">{{@model.description}}</p> + <PluginOutlet + @name="about-after-description" + @connectorTagName="section" + @outletArgs={{hash model=this.model}} + /> + </section> + <div class="about__main-content"> + <section class="about__left-side"> + <div class="about__stats"> + {{#each this.stats as |stat|}} + <span class="about__stats-item {{stat.class}}"> + {{dIcon stat.icon}} + <span>{{stat.text}}</span> + </span> + {{/each}} + </div> + <h3>{{i18n "about.simple_title"}}</h3> + <div>{{htmlSafe @model.extended_site_description}}</div> + </section> + <section class="about__right-side"> + <h4>{{i18n "about.contact"}}</h4> + {{#if this.contactInfo}} + <p>{{htmlSafe this.contactInfo}}</p> + {{/if}} + <p>{{i18n "about.report_inappropriate_content"}}</p> + </section> + </div> + </template> +} diff --git a/app/assets/javascripts/discourse/app/templates/about.hbs b/app/assets/javascripts/discourse/app/templates/about.hbs index e9524d335d4..25bfe465e22 100644 --- a/app/assets/javascripts/discourse/app/templates/about.hbs +++ b/app/assets/javascripts/discourse/app/templates/about.hbs @@ -35,147 +35,151 @@ }}</LinkTo></li> {{/if}} </ul> - - <section class="about description"> - <h2>{{i18n "about.title" title=this.model.title}}</h2> - <p>{{this.model.description}}</p> - </section> - - <PluginOutlet - @name="about-after-description" - @connectorTagName="section" - @outletArgs={{hash model=this.model}} - /> - - {{#if this.model.admins}} - <section class="about admins"> - <h3>{{d-icon "users"}} {{i18n "about.our_admins"}}</h3> - <div class="users"> - <AboutPageUsers @users={{this.model.admins}} /> - </div> + {{#if this.currentUser.render_experimental_about_page}} + <AboutPage @model={{this.model}} /> + {{else}} + <section class="about description"> + <h2>{{i18n "about.title" title=this.model.title}}</h2> + <p>{{this.model.description}}</p> </section> - {{/if}} - <span> <PluginOutlet - @name="about-after-admins" + @name="about-after-description" @connectorTagName="section" @outletArgs={{hash model=this.model}} /> - </span> - {{#if this.model.moderators}} - <section class="about moderators"> - <h3>{{d-icon "users"}} {{i18n "about.our_moderators"}}</h3> - <div class="users"> - <AboutPageUsers @users={{this.model.moderators}} /> - </div> - </section> - {{/if}} - - <span> - <PluginOutlet - @name="about-after-moderators" - @connectorTagName="section" - @outletArgs={{hash model=this.model}} - /> - </span> - - {{#if this.model.category_moderators.length}} - {{#each this.model.category_moderators as |cm|}} - <section - class="about category-moderators moderators-{{cm.category.slug}}" - > - <h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3> + {{#if this.model.admins}} + <section class="about admins"> + <h3>{{d-icon "users"}} {{i18n "about.our_admins"}}</h3> <div class="users"> - <AboutPageUsers @users={{cm.moderators}} /> + <AboutPageUsers @users={{this.model.admins}} /> </div> - <div class="clearfix"></div> </section> - {{/each}} - {{/if}} - {{#if this.model.can_see_about_stats}} - <section class="about stats"> - <h3>{{d-icon "far-chart-bar"}} {{i18n "about.stats"}}</h3> - <table class="table"> - <thead> - <tr> - <th> - </th> - <th>{{i18n "about.stat.last_day"}}</th> - <th>{{i18n "about.stat.last_7_days"}}</th> - <th>{{i18n "about.stat.last_30_days"}}</th> - <th>{{i18n "about.stat.all_time"}}</th> - </tr> - </thead> - <tbody> - <tr class="about-topic-count"> - <td class="title">{{i18n "about.topic_count"}}</td> - <td>{{number this.model.stats.topics_last_day}}</td> - <td>{{number this.model.stats.topics_7_days}}</td> - <td>{{number this.model.stats.topics_30_days}}</td> - <td>{{number this.model.stats.topics_count}}</td> - </tr> - <tr class="about-post-count"> - <td>{{i18n "about.post_count"}}</td> - <td>{{number this.model.stats.posts_last_day}}</td> - <td>{{number this.model.stats.posts_7_days}}</td> - <td>{{number this.model.stats.posts_30_days}}</td> - <td>{{number this.model.stats.posts_count}}</td> - </tr> - <tr class="about-user-count"> - <td>{{i18n "about.user_count"}}</td> - <td>{{number this.model.stats.users_last_day}}</td> - <td>{{number this.model.stats.users_7_days}}</td> - <td>{{number this.model.stats.users_30_days}}</td> - <td>{{number this.model.stats.users_count}}</td> - </tr> - <tr class="about-active-user-count"> - <td>{{i18n "about.active_user_count"}}</td> - <td>{{number this.model.stats.active_users_last_day}}</td> - <td>{{number this.model.stats.active_users_7_days}}</td> - <td>{{number this.model.stats.active_users_30_days}}</td> - <td>—</td> - </tr> - <tr class="about-like-count"> - <td>{{i18n "about.like_count"}}</td> - <td>{{number this.model.stats.likes_last_day}}</td> - <td>{{number this.model.stats.likes_7_days}}</td> - <td>{{number this.model.stats.likes_30_days}}</td> - <td>{{number this.model.stats.likes_count}}</td> - </tr> - {{#each - this.site.displayed_about_plugin_stat_groups - as |statGroupName| - }} - <tr class={{concat "about-" statGroupName "-count"}}> - <td>{{i18n (concat "about." statGroupName "_count")}}</td> - <td>{{number - (get this.model.stats (concat statGroupName "_last_day")) - }}</td> - <td>{{number - (get this.model.stats (concat statGroupName "_7_days")) - }}</td> - <td>{{number - (get this.model.stats (concat statGroupName "_30_days")) - }}</td> - <td>{{number - (get this.model.stats (concat statGroupName "_count")) - }}</td> + {{/if}} + + <span> + <PluginOutlet + @name="about-after-admins" + @connectorTagName="section" + @outletArgs={{hash model=this.model}} + /> + </span> + + {{#if this.model.moderators}} + <section class="about moderators"> + <h3>{{d-icon "users"}} {{i18n "about.our_moderators"}}</h3> + <div class="users"> + <AboutPageUsers @users={{this.model.moderators}} /> + </div> + </section> + {{/if}} + + <span> + <PluginOutlet + @name="about-after-moderators" + @connectorTagName="section" + @outletArgs={{hash model=this.model}} + /> + </span> + + {{#if this.model.category_moderators.length}} + {{#each this.model.category_moderators as |cm|}} + <section + class="about category-moderators moderators-{{cm.category.slug}}" + > + <h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3> + <div class="users"> + <AboutPageUsers @users={{cm.moderators}} /> + </div> + <div class="clearfix"></div> + </section> + {{/each}} + {{/if}} + {{#if this.model.can_see_about_stats}} + <section class="about stats"> + <h3>{{d-icon "far-chart-bar"}} {{i18n "about.stats"}}</h3> + <table class="table"> + <thead> + <tr> + <th> + </th> + <th>{{i18n "about.stat.last_day"}}</th> + <th>{{i18n "about.stat.last_7_days"}}</th> + <th>{{i18n "about.stat.last_30_days"}}</th> + <th>{{i18n "about.stat.all_time"}}</th> </tr> - {{/each}} - </tbody> - </table> - </section> - {{/if}} + </thead> + <tbody> + <tr class="about-topic-count"> + <td class="title">{{i18n "about.topic_count"}}</td> + <td>{{number this.model.stats.topics_last_day}}</td> + <td>{{number this.model.stats.topics_7_days}}</td> + <td>{{number this.model.stats.topics_30_days}}</td> + <td>{{number this.model.stats.topics_count}}</td> + </tr> + <tr class="about-post-count"> + <td>{{i18n "about.post_count"}}</td> + <td>{{number this.model.stats.posts_last_day}}</td> + <td>{{number this.model.stats.posts_7_days}}</td> + <td>{{number this.model.stats.posts_30_days}}</td> + <td>{{number this.model.stats.posts_count}}</td> + </tr> + <tr class="about-user-count"> + <td>{{i18n "about.user_count"}}</td> + <td>{{number this.model.stats.users_last_day}}</td> + <td>{{number this.model.stats.users_7_days}}</td> + <td>{{number this.model.stats.users_30_days}}</td> + <td>{{number this.model.stats.users_count}}</td> + </tr> + <tr class="about-active-user-count"> + <td>{{i18n "about.active_user_count"}}</td> + <td>{{number this.model.stats.active_users_last_day}}</td> + <td>{{number this.model.stats.active_users_7_days}}</td> + <td>{{number this.model.stats.active_users_30_days}}</td> + <td>—</td> + </tr> + <tr class="about-like-count"> + <td>{{i18n "about.like_count"}}</td> + <td>{{number this.model.stats.likes_last_day}}</td> + <td>{{number this.model.stats.likes_7_days}}</td> + <td>{{number this.model.stats.likes_30_days}}</td> + <td>{{number this.model.stats.likes_count}}</td> + </tr> + {{#each + this.site.displayed_about_plugin_stat_groups + as |statGroupName| + }} + <tr class={{concat "about-" statGroupName "-count"}}> + <td>{{i18n (concat "about." statGroupName "_count")}}</td> + <td>{{number + (get + this.model.stats (concat statGroupName "_last_day") + ) + }}</td> + <td>{{number + (get this.model.stats (concat statGroupName "_7_days")) + }}</td> + <td>{{number + (get this.model.stats (concat statGroupName "_30_days")) + }}</td> + <td>{{number + (get this.model.stats (concat statGroupName "_count")) + }}</td> + </tr> + {{/each}} + </tbody> + </table> + </section> + {{/if}} - {{#if this.contactInfo}} - <section class="about contact"> - <h3>{{d-icon "envelope"}} {{i18n "about.contact"}}</h3> - <p>{{html-safe this.contactInfo}}</p> - </section> + {{#if this.contactInfo}} + <section class="about contact"> + <h3>{{d-icon "envelope"}} {{i18n "about.contact"}}</h3> + <p>{{html-safe this.contactInfo}}</p> + </section> + {{/if}} {{/if}} - </div> </div> </section> \ No newline at end of file diff --git a/app/assets/stylesheets/common/base/about.scss b/app/assets/stylesheets/common/base/about.scss index bd4ce19f550..4a8af5354f5 100644 --- a/app/assets/stylesheets/common/base/about.scss +++ b/app/assets/stylesheets/common/base/about.scss @@ -1,3 +1,30 @@ +.about { + &__main-content { + display: grid; + grid-template-columns: 2fr 1fr; + column-gap: 4em; + } + + &__stats { + display: flex; + border-top: 1px solid var(--primary-low); + border-bottom: 1px solid var(--primary-low); + padding: 1em 1em; + margin-bottom: 1em; + } + + &__stats-item { + flex-grow: 1; + flex-basis: 0; + } + + &__banner { + margin-bottom: 1em; + min-height: 300px; + max-height: 300px; + } +} + section.about { margin-bottom: 3em; diff --git a/app/models/about.rb b/app/models/about.rb index 476e597d35d..0cc71542464 100644 --- a/app/models/about.rb +++ b/app/models/about.rb @@ -54,6 +54,16 @@ class About SiteSetting.site_description end + def extended_site_description + SiteSetting.extended_site_description_cooked + end + + def banner_image + url = SiteSetting.about_banner_image&.url + return if url.blank? + GlobalPath.full_cdn_url(url) + end + def moderators @moderators ||= User.where(moderator: true, admin: false).human_users.order("last_seen_at DESC") end diff --git a/app/serializers/about_serializer.rb b/app/serializers/about_serializer.rb index b4bee4378d4..c5961242988 100644 --- a/app/serializers/about_serializer.rb +++ b/app/serializers/about_serializer.rb @@ -20,6 +20,8 @@ class AboutSerializer < ApplicationSerializer attributes :stats, :description, + :extended_site_description, + :banner_image, :title, :locale, :version, @@ -52,6 +54,14 @@ class AboutSerializer < ApplicationSerializer SiteSetting.contact_email end + def include_extended_site_description? + render_redesigned_about_page? + end + + def include_banner_image? + render_redesigned_about_page? + end + private def can_see_about_stats @@ -61,4 +71,10 @@ class AboutSerializer < ApplicationSerializer def can_see_site_contact_details scope.can_see_site_contact_details? end + + def render_redesigned_about_page? + return false if scope.anonymous? + + scope.user.in_any_groups?(SiteSetting.experimental_redesigned_about_page_groups_map) + end end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index e678e6fcf73..3a4b00dd321 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -77,7 +77,8 @@ class CurrentUserSerializer < BasicUserSerializer :can_view_raw_email, :use_glimmer_topic_list?, :login_method, - :show_experimental_flags_admin_page + :show_experimental_flags_admin_page, + :render_experimental_about_page delegate :user_stat, to: :object, private: true delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat @@ -146,6 +147,10 @@ class CurrentUserSerializer < BasicUserSerializer object.in_any_groups?(SiteSetting.experimental_flags_admin_page_enabled_groups_map) end + def render_experimental_about_page + object.in_any_groups?(SiteSetting.experimental_redesigned_about_page_groups_map) + end + def include_show_experimental_flags_admin_page? object.admin? end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e30ded8fe0d..9f8c807678c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -344,6 +344,16 @@ en: active_user_count: "Active users" contact: "Contact us" contact_info: "In the event of a critical issue or urgent matter affecting this site, please contact us at %{contact_info}." + member_count: + one: "%{formatted_number} Member" + other: "%{formatted_number} Members" + admin_count: + one: "%{formatted_number} Admin" + other: "%{formatted_number} Admins" + moderator_count: + one: "%{formatted_number} Moderator" + other: "%{formatted_number} Moderators" + report_inappropriate_content: "If you come across any inappropriate content, don't hesitate to start a conversation with our moderators and admins. Remember to log in before reaching out." bookmarked: title: "Bookmark" diff --git a/config/site_settings.yml b/config/site_settings.yml index f2737aa5b3b..db05035ed79 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2437,6 +2437,11 @@ developer: list_type: compact allow_any: false refresh: true + experimental_redesigned_about_page_groups: + default: "" + type: group_list + hidden: true + allow_any: false navigation: navigation_menu: diff --git a/spec/system/about_page_spec.rb b/spec/system/about_page_spec.rb new file mode 100644 index 00000000000..7fbf030e4b7 --- /dev/null +++ b/spec/system/about_page_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +describe "About page", type: :system do + fab!(:current_user) { Fabricate(:user) } + fab!(:group) { Fabricate(:group, users: [current_user]) } + fab!(:image_upload) + fab!(:admin) { Fabricate(:admin, last_seen_at: 1.hour.ago) } + fab!(:moderator) { Fabricate(:moderator, last_seen_at: 1.hour.ago) } + + before do + SiteSetting.title = "title for my forum" + SiteSetting.site_description = "short description for my forum" + SiteSetting.extended_site_description = <<~TEXT + Somewhat lengthy description for my **forum**. [Some link](https://discourse.org). A list: + 1. One + 2. Two + Last line. + TEXT + SiteSetting.extended_site_description_cooked = + PrettyText.markdown(SiteSetting.extended_site_description) + SiteSetting.about_banner_image = image_upload + SiteSetting.contact_url = "http://some-contact-url.discourse.org" + end + + describe "legacy version" do + it "renders successfully for a logged-in user" do + sign_in(current_user) + + visit("/about") + + expect(page).to have_css(".about.admins") + expect(page).to have_css(".about.moderators") + expect(page).to have_css(".about.stats") + expect(page).to have_css(".about.contact") + end + + it "renders successfully for an anonymous user" do + visit("/about") + + expect(page).to have_css(".about.admins") + expect(page).to have_css(".about.moderators") + expect(page).to have_css(".about.stats") + expect(page).to have_css(".about.contact") + end + end + + describe "redesigned version" do + let(:about_page) { PageObjects::Pages::About.new } + + before do + SiteSetting.experimental_redesigned_about_page_groups = group.id.to_s + sign_in(current_user) + end + + it "renders successfully for a logged in user" do + about_page.visit + + expect(about_page).to have_banner_image(image_upload) + expect(about_page).to have_header_title(SiteSetting.title) + expect(about_page).to have_short_description(SiteSetting.site_description) + + expect(about_page).to have_members_count(4, "4") + expect(about_page).to have_admins_count(1, "1") + expect(about_page).to have_moderators_count(1, "1") + end + end +end diff --git a/spec/system/page_objects/pages/about.rb b/spec/system/page_objects/pages/about.rb new file mode 100644 index 00000000000..06fb85cb59f --- /dev/null +++ b/spec/system/page_objects/pages/about.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module PageObjects + module Pages + class About < PageObjects::Pages::Base + def visit + page.visit("/about") + end + + def has_header_title?(title) + has_css?(".about__header h3", text: title) + end + + def has_short_description?(content) + has_css?(".about__header .short-description", text: content) + end + + def has_banner_image?(upload) + has_css?("img.about__banner[src=\"#{GlobalPath.full_cdn_url(upload.url)}\"]") + end + + def has_members_count?(count, formatted_number) + element = find(".about__stats-item.members span") + element.has_text?(I18n.t("js.about.member_count", count:, formatted_number:)) + end + + def has_admins_count?(count, formatted_number) + element = find(".about__stats-item.admins span") + element.has_text?(I18n.t("js.about.admin_count", count:, formatted_number:)) + end + + def has_moderators_count?(count, formatted_number) + element = find(".about__stats-item.moderators span") + element.has_text?(I18n.t("js.about.moderator_count", count:, formatted_number:)) + end + end + end +end