DEV: Initial parts for a redesigned /about page ()

This commit introduces the foundation for a new design for the /about page that we're currently working on.  The current version will remain available and still be the default until we finish the new version and are ready to roll out. To opt into the new version right now, add one or more group to the `experimental_redesigned_about_page_groups` site setting and members in those groups will get the new version.

Internal topic: t/128545.
This commit is contained in:
Osama Sayegh 2024-07-23 01:35:18 +03:00 committed by GitHub
parent 2d59795e28
commit 6039b513fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 413 additions and 129 deletions
app
assets
javascripts/discourse/app
components
templates
stylesheets/common/base
models
serializers
config
spec/system
about_page_spec.rb
page_objects/pages

@ -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>
}

@ -35,147 +35,151 @@
}}</LinkTo></li> }}</LinkTo></li>
{{/if}} {{/if}}
</ul> </ul>
{{#if this.currentUser.render_experimental_about_page}}
<section class="about description"> <AboutPage @model={{this.model}} />
<h2>{{i18n "about.title" title=this.model.title}}</h2> {{else}}
<p>{{this.model.description}}</p> <section class="about description">
</section> <h2>{{i18n "about.title" title=this.model.title}}</h2>
<p>{{this.model.description}}</p>
<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>
</section> </section>
{{/if}}
<span>
<PluginOutlet <PluginOutlet
@name="about-after-admins" @name="about-after-description"
@connectorTagName="section" @connectorTagName="section"
@outletArgs={{hash model=this.model}} @outletArgs={{hash model=this.model}}
/> />
</span>
{{#if this.model.moderators}} {{#if this.model.admins}}
<section class="about moderators"> <section class="about admins">
<h3>{{d-icon "users"}} {{i18n "about.our_moderators"}}</h3> <h3>{{d-icon "users"}} {{i18n "about.our_admins"}}</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"> <div class="users">
<AboutPageUsers @users={{cm.moderators}} /> <AboutPageUsers @users={{this.model.admins}} />
</div> </div>
<div class="clearfix"></div>
</section> </section>
{{/each}} {{/if}}
{{/if}}
{{#if this.model.can_see_about_stats}} <span>
<section class="about stats"> <PluginOutlet
<h3>{{d-icon "far-chart-bar"}} {{i18n "about.stats"}}</h3> @name="about-after-admins"
<table class="table"> @connectorTagName="section"
<thead> @outletArgs={{hash model=this.model}}
<tr> />
<th> </span>
</th>
<th>{{i18n "about.stat.last_day"}}</th> {{#if this.model.moderators}}
<th>{{i18n "about.stat.last_7_days"}}</th> <section class="about moderators">
<th>{{i18n "about.stat.last_30_days"}}</th> <h3>{{d-icon "users"}} {{i18n "about.our_moderators"}}</h3>
<th>{{i18n "about.stat.all_time"}}</th> <div class="users">
</tr> <AboutPageUsers @users={{this.model.moderators}} />
</thead> </div>
<tbody> </section>
<tr class="about-topic-count"> {{/if}}
<td class="title">{{i18n "about.topic_count"}}</td>
<td>{{number this.model.stats.topics_last_day}}</td> <span>
<td>{{number this.model.stats.topics_7_days}}</td> <PluginOutlet
<td>{{number this.model.stats.topics_30_days}}</td> @name="about-after-moderators"
<td>{{number this.model.stats.topics_count}}</td> @connectorTagName="section"
</tr> @outletArgs={{hash model=this.model}}
<tr class="about-post-count"> />
<td>{{i18n "about.post_count"}}</td> </span>
<td>{{number this.model.stats.posts_last_day}}</td>
<td>{{number this.model.stats.posts_7_days}}</td> {{#if this.model.category_moderators.length}}
<td>{{number this.model.stats.posts_30_days}}</td> {{#each this.model.category_moderators as |cm|}}
<td>{{number this.model.stats.posts_count}}</td> <section
</tr> class="about category-moderators moderators-{{cm.category.slug}}"
<tr class="about-user-count"> >
<td>{{i18n "about.user_count"}}</td> <h3>{{category-link cm.category}}{{i18n "about.moderators"}}</h3>
<td>{{number this.model.stats.users_last_day}}</td> <div class="users">
<td>{{number this.model.stats.users_7_days}}</td> <AboutPageUsers @users={{cm.moderators}} />
<td>{{number this.model.stats.users_30_days}}</td> </div>
<td>{{number this.model.stats.users_count}}</td> <div class="clearfix"></div>
</tr> </section>
<tr class="about-active-user-count"> {{/each}}
<td>{{i18n "about.active_user_count"}}</td> {{/if}}
<td>{{number this.model.stats.active_users_last_day}}</td> {{#if this.model.can_see_about_stats}}
<td>{{number this.model.stats.active_users_7_days}}</td> <section class="about stats">
<td>{{number this.model.stats.active_users_30_days}}</td> <h3>{{d-icon "far-chart-bar"}} {{i18n "about.stats"}}</h3>
<td>&mdash;</td> <table class="table">
</tr> <thead>
<tr class="about-like-count"> <tr>
<td>{{i18n "about.like_count"}}</td> <th>
<td>{{number this.model.stats.likes_last_day}}</td> </th>
<td>{{number this.model.stats.likes_7_days}}</td> <th>{{i18n "about.stat.last_day"}}</th>
<td>{{number this.model.stats.likes_30_days}}</td> <th>{{i18n "about.stat.last_7_days"}}</th>
<td>{{number this.model.stats.likes_count}}</td> <th>{{i18n "about.stat.last_30_days"}}</th>
</tr> <th>{{i18n "about.stat.all_time"}}</th>
{{#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> </tr>
{{/each}} </thead>
</tbody> <tbody>
</table> <tr class="about-topic-count">
</section> <td class="title">{{i18n "about.topic_count"}}</td>
{{/if}} <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>&mdash;</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}} {{#if this.contactInfo}}
<section class="about contact"> <section class="about contact">
<h3>{{d-icon "envelope"}} {{i18n "about.contact"}}</h3> <h3>{{d-icon "envelope"}} {{i18n "about.contact"}}</h3>
<p>{{html-safe this.contactInfo}}</p> <p>{{html-safe this.contactInfo}}</p>
</section> </section>
{{/if}}
{{/if}} {{/if}}
</div> </div>
</div> </div>
</section> </section>

@ -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 { section.about {
margin-bottom: 3em; margin-bottom: 3em;

@ -54,6 +54,16 @@ class About
SiteSetting.site_description SiteSetting.site_description
end 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 def moderators
@moderators ||= User.where(moderator: true, admin: false).human_users.order("last_seen_at DESC") @moderators ||= User.where(moderator: true, admin: false).human_users.order("last_seen_at DESC")
end end

@ -20,6 +20,8 @@ class AboutSerializer < ApplicationSerializer
attributes :stats, attributes :stats,
:description, :description,
:extended_site_description,
:banner_image,
:title, :title,
:locale, :locale,
:version, :version,
@ -52,6 +54,14 @@ class AboutSerializer < ApplicationSerializer
SiteSetting.contact_email SiteSetting.contact_email
end end
def include_extended_site_description?
render_redesigned_about_page?
end
def include_banner_image?
render_redesigned_about_page?
end
private private
def can_see_about_stats def can_see_about_stats
@ -61,4 +71,10 @@ class AboutSerializer < ApplicationSerializer
def can_see_site_contact_details def can_see_site_contact_details
scope.can_see_site_contact_details? scope.can_see_site_contact_details?
end 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 end

@ -77,7 +77,8 @@ class CurrentUserSerializer < BasicUserSerializer
:can_view_raw_email, :can_view_raw_email,
:use_glimmer_topic_list?, :use_glimmer_topic_list?,
:login_method, :login_method,
:show_experimental_flags_admin_page :show_experimental_flags_admin_page,
:render_experimental_about_page
delegate :user_stat, to: :object, private: true delegate :user_stat, to: :object, private: true
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat 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) object.in_any_groups?(SiteSetting.experimental_flags_admin_page_enabled_groups_map)
end 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? def include_show_experimental_flags_admin_page?
object.admin? object.admin?
end end

@ -344,6 +344,16 @@ en:
active_user_count: "Active users" active_user_count: "Active users"
contact: "Contact us" contact: "Contact us"
contact_info: "In the event of a critical issue or urgent matter affecting this site, please contact us at %{contact_info}." 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: bookmarked:
title: "Bookmark" title: "Bookmark"

@ -2437,6 +2437,11 @@ developer:
list_type: compact list_type: compact
allow_any: false allow_any: false
refresh: true refresh: true
experimental_redesigned_about_page_groups:
default: ""
type: group_list
hidden: true
allow_any: false
navigation: navigation:
navigation_menu: navigation_menu:

@ -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

@ -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