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

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

View File

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

View File

@ -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>&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>
{{/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>&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}}
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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