Added Subheads behind a flag (#20265)

refs MOM-152 MOM-148 MOM-151

- Added Subheads behind a flag + toggle in settings.
- Removes Excerpt fields from post settings if flag is enabled.
- Added subhead toggle in newsletter settings.
- Loads of styling

---------

Co-authored-by: Sanne de Vries <sannedv@protonmail.com>
This commit is contained in:
Ronald Langeveld 2024-05-29 16:53:40 +07:00 committed by GitHub
parent 9099ab47c4
commit fddcf3ffee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 187 additions and 88 deletions

View File

@ -21,6 +21,7 @@ export type Newsletter = {
show_header_title: boolean; show_header_title: boolean;
title_font_category: string; title_font_category: string;
title_alignment: string; title_alignment: string;
show_subhead: boolean;
show_feature_image: boolean; show_feature_image: boolean;
body_font_category: string; body_font_category: string;
footer_content: string | null; footer_content: string | null;

View File

@ -19,6 +19,7 @@
"show_header_title": true, "show_header_title": true,
"title_font_category": "serif", "title_font_category": "serif",
"title_alignment": "center", "title_alignment": "center",
"show_subhead": true,
"show_feature_image": true, "show_feature_image": true,
"body_font_category": "serif", "body_font_category": "serif",
"footer_content": "", "footer_content": "",

View File

@ -67,6 +67,10 @@ const features = [{
title: 'ActivityPub', title: 'ActivityPub',
description: '(Highly) Experimental support for ActivityPub.', description: '(Highly) Experimental support for ActivityPub.',
flag: 'ActivityPub' flag: 'ActivityPub'
},{
title: 'Subhead',
description: 'Using custom excerpts as subheads in editor and newsletter',
flag: 'subhead'
}]; }];
const AlphaFeatures: React.FC = () => { const AlphaFeatures: React.FC = () => {

View File

@ -103,6 +103,7 @@ const Sidebar: React.FC<{
const {mutateAsync: uploadImage} = useUploadImage(); const {mutateAsync: uploadImage} = useUploadImage();
const [selectedTab, setSelectedTab] = useState('generalSettings'); const [selectedTab, setSelectedTab] = useState('generalSettings');
const hasEmailCustomization = useFeatureFlag('emailCustomization'); const hasEmailCustomization = useFeatureFlag('emailCustomization');
const hasSubhead = useFeatureFlag('subhead');
const {localSettings} = useSettingGroup(); const {localSettings} = useSettingGroup();
const [siteTitle] = getSettingValues(localSettings, ['title']) as string[]; const [siteTitle] = getSettingValues(localSettings, ['title']) as string[];
const handleError = useHandleError(); const handleError = useHandleError();
@ -423,6 +424,14 @@ const Sidebar: React.FC<{
title='Body style' title='Body style'
onSelect={option => updateNewsletter({body_font_category: option?.value})} onSelect={option => updateNewsletter({body_font_category: option?.value})}
/> />
{hasSubhead &&
<Toggle
checked={newsletter.show_subhead}
direction="rtl"
label='Subhead'
onChange={e => updateNewsletter({show_subhead: e.target.checked})}
/>
}
<Toggle <Toggle
checked={newsletter.show_feature_image} checked={newsletter.show_feature_image}
direction="rtl" direction="rtl"

View File

@ -110,6 +110,7 @@ const NewsletterPreview: React.FC<{newsletter: Newsletter}> = ({newsletter}) =>
showFeedback={showFeedback} showFeedback={showFeedback}
showLatestPosts={newsletter.show_latest_posts} showLatestPosts={newsletter.show_latest_posts}
showPostTitleSection={newsletter.show_post_title_section} showPostTitleSection={newsletter.show_post_title_section}
showSubhead={newsletter.show_subhead}
showSubscriptionDetails={newsletter.show_subscription_details} showSubscriptionDetails={newsletter.show_subscription_details}
siteTitle={title} siteTitle={title}
titleAlignment={newsletter.title_alignment} titleAlignment={newsletter.title_alignment}

View File

@ -18,6 +18,7 @@ const NewsletterPreviewContent: React.FC<{
headerTitle?: string | null; headerTitle?: string | null;
headerSubtitle?: string | null; headerSubtitle?: string | null;
showPostTitleSection: boolean; showPostTitleSection: boolean;
showSubhead: boolean;
titleAlignment?: string; titleAlignment?: string;
titleFontCategory?: string; titleFontCategory?: string;
bodyFontCategory?: string; bodyFontCategory?: string;
@ -49,6 +50,7 @@ const NewsletterPreviewContent: React.FC<{
headerTitle, headerTitle,
headerSubtitle, headerSubtitle,
showPostTitleSection, showPostTitleSection,
showSubhead,
titleAlignment, titleAlignment,
titleFontCategory, titleFontCategory,
bodyFontCategory, bodyFontCategory,
@ -75,6 +77,7 @@ const NewsletterPreviewContent: React.FC<{
const showHeader = headerIcon || headerTitle; const showHeader = headerIcon || headerTitle;
const {config} = useGlobalData(); const {config} = useGlobalData();
const hasNewEmailAddresses = useFeatureFlag('newEmailAddresses'); const hasNewEmailAddresses = useFeatureFlag('newEmailAddresses');
const hasSubhead = useFeatureFlag('subhead');
const currentDate = new Date().toLocaleDateString('default', { const currentDate = new Date().toLocaleDateString('default', {
year: 'numeric', year: 'numeric',
@ -131,6 +134,9 @@ const NewsletterPreviewContent: React.FC<{
)} style={{color: titleColor}}> )} style={{color: titleColor}}>
Your email newsletter Your email newsletter
</h2> </h2>
{(hasSubhead && showSubhead) && (
<p className="mb-4 text-[1.6rem] leading-[1.7] text-black">A subhead that highlights the key points of your newsletter.</p>
)}
<div className={clsx( <div className={clsx(
'flex w-full justify-between text-center text-md leading-none text-grey-700', 'flex w-full justify-between text-center text-md leading-none text-grey-700',
titleAlignment === 'center' ? 'flex-col' : 'flex-row' titleAlignment === 'center' ? 'flex-col' : 'flex-row'

View File

@ -56,6 +56,22 @@
{{on "paste" this.cleanPastedTitle}} {{on "paste" this.cleanPastedTitle}}
data-test-editor-title-input={{true}} data-test-editor-title-input={{true}}
/> />
{{#if (feature 'subhead')}}
<GhTextarea
@class="gh-editor-subhead"
@placeholder="Add a subhead..."
@shouldFocus={{false}}
@tabindex="1"
@autoExpand=".gh-koenig-editor"
@value={{readonly this.excerpt}}
@input={{this.updateExcerpt}}
@focus-out={{optional @blurExcerpt}}
@keyDown={{this.onExcerptKeydown}}
data-test-editor-subhead-input={{true}}
/>
<hr class="gh-editor-title-divider">
{{/if}}
</div> </div>
<KoenigLexicalEditor <KoenigLexicalEditor

View File

@ -6,6 +6,7 @@ import {tracked} from '@glimmer/tracking';
export default class GhKoenigEditorReactComponent extends Component { export default class GhKoenigEditorReactComponent extends Component {
@service settings; @service settings;
@service feature;
containerElement = null; containerElement = null;
titleElement = null; titleElement = null;
@ -30,6 +31,10 @@ export default class GhKoenigEditorReactComponent extends Component {
return color; return color;
} }
get excerpt() {
return this.args.excerpt || '';
}
@action @action
registerElement(element) { registerElement(element) {
this.containerElement = element; this.containerElement = element;
@ -112,6 +117,15 @@ export default class GhKoenigEditorReactComponent extends Component {
// - Enter, creating an empty paragraph when editor is not empty // - Enter, creating an empty paragraph when editor is not empty
@action @action
onTitleKeydown(event) { onTitleKeydown(event) {
if (this.feature.get('subhead')) {
if (event.key === 'Enter') {
event.preventDefault();
const subheadElement = document.querySelector('.gh-editor-subhead');
if (subheadElement) {
subheadElement.focus();
}
}
} else {
const {editorAPI} = this; const {editorAPI} = this;
if (!editorAPI || event.originalEvent.isComposing) { if (!editorAPI || event.originalEvent.isComposing) {
@ -134,6 +148,34 @@ export default class GhKoenigEditorReactComponent extends Component {
} }
} }
} }
}
// Subheader ("excerpt") Actions -------------------------------------------
@action
updateExcerpt(event) {
this.args.onExcerptChange?.(event.target.value);
}
@action
focusExcerpt() {
this.args.onExcerptFocus?.();
}
@action
blurExcerpt() {
this.args.onExcerptBlur?.();
}
@action
onExcerptKeydown(event) {
if (event.key === 'Enter') {
event.preventDefault();
this.editorAPI.focusEditor({position: 'top'});
}
}
// move cursor to the editor on
// Body actions ------------------------------------------------------------ // Body actions ------------------------------------------------------------

View File

@ -93,7 +93,7 @@
{{/if}} {{/if}}
{{/if}} {{/if}}
{{#unless (feature 'subhead')}}
<GhFormGroup @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="customExcerpt"> <GhFormGroup @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="customExcerpt">
<label for="custom-excerpt">Excerpt</label> <label for="custom-excerpt">Excerpt</label>
<GhTextarea <GhTextarea
@ -108,6 +108,7 @@
/> />
<GhErrorMessage @errors={{this.post.errors}} @property="customExcerpt" data-test-error="custom-excerpt" /> <GhErrorMessage @errors={{this.post.errors}} @property="customExcerpt" data-test-error="custom-excerpt" />
</GhFormGroup> </GhFormGroup>
{{/unless}}
{{#unless this.session.user.isAuthorOrContributor}} {{#unless this.session.user.isAuthorOrContributor}}
<GhFormGroup class="for-select mb8" @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="authors" data-test-input="authors"> <GhFormGroup class="for-select mb8" @errors={{this.post.errors}} @hasValidated={{this.post.hasValidated}} @property="authors" data-test-input="authors">

View File

@ -284,6 +284,11 @@ export default class LexicalEditorController extends Controller {
this.set('post.titleScratch', title); this.set('post.titleScratch', title);
} }
@action
updateExcerptScratch(excerpt) {
this.set('post.customExcerptScratch', excerpt);
}
// updates local willPublish/Schedule values, does not get applied to // updates local willPublish/Schedule values, does not get applied to
// the post's `status` value until a save is triggered // the post's `status` value until a save is triggered
@action @action

View File

@ -24,6 +24,7 @@ export default class Newsletter extends Model.extend(ValidationEngine) {
@attr({defaultValue: true}) showHeaderTitle; @attr({defaultValue: true}) showHeaderTitle;
@attr({defaultValue: true}) showHeaderName; @attr({defaultValue: true}) showHeaderName;
@attr({defaultValue: true}) showPostTitleSection; @attr({defaultValue: true}) showPostTitleSection;
@attr({defaultValue: false}) showSubhead;
@attr({defaultValue: true}) showCommentCta; @attr({defaultValue: true}) showCommentCta;
@attr({defaultValue: false}) showSubscriptionDetails; @attr({defaultValue: false}) showSubscriptionDetails;
@attr({defaultValue: false}) showLatestPosts; @attr({defaultValue: false}) showLatestPosts;

View File

@ -83,6 +83,7 @@ export default class FeatureService extends Service {
@feature('onboardingChecklist') onboardingChecklist; @feature('onboardingChecklist') onboardingChecklist;
@feature('ActivityPub') ActivityPub; @feature('ActivityPub') ActivityPub;
@feature('internalLinking') internalLinking; @feature('internalLinking') internalLinking;
@feature('subhead') subhead;
_user = null; _user = null;

View File

@ -681,7 +681,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
.gh-editor-feature-image-caption { .gh-editor-feature-image-caption {
width: 100%; width: 100%;
min-height: 24px; min-height: 24px;
margin: 0 0 1.7em 0; margin: 0 0 1.2rem 0;
padding: 0; padding: 0;
outline: none; outline: none;
border-width: 0; border-width: 0;
@ -717,12 +717,25 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
opacity: .5; opacity: .5;
} }
.gh-editor-title-container {
position: relative;
max-width: 740px;
width: 100%;
margin-right: auto;
margin-left: auto;
border: none;
background: transparent;
}
.gh-editor-title { .gh-editor-title {
display: block; display: block;
width: 100%; width: 100%;
max-width: unset;
min-height: auto; min-height: auto;
margin-bottom: 1.2rem; margin: 0 0 1.6rem;
padding: 0 0 4px;
border: none; border: none;
background: transparent;
color: var(--black); color: var(--black);
font-size: 4.8rem; font-size: 4.8rem;
letter-spacing: -0.017em; letter-spacing: -0.017em;
@ -732,6 +745,18 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
box-shadow: none; box-shadow: none;
} }
@media (min-width: 500px) and (max-width: 768px) {
.gh-editor-title {
font-size: 3.6rem;
}
}
@media (max-width: 500px) {
.gh-editor-title {
font-size: 2.8rem;
}
}
.gh-editor-title:focus { .gh-editor-title:focus {
box-shadow: none !important; box-shadow: none !important;
border: none !important; border: none !important;
@ -741,6 +766,55 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
opacity: .5; opacity: .5;
} }
.gh-editor-title::placeholder {
color: var(--lightgrey-d1);
font-weight: 700;
opacity: 1;
}
.gh-editor-hidden-indicator {
position: absolute;
top: -1px;
height: 2.4rem;
margin-left: -6rem;
color: var(--midgrey-l2);
}
.gh-editor-title-container .gh-editor-hidden-indicator {
top: 1.8rem;
}
.gh-editor-hidden-indicator svg {
height: 2.4rem;
}
.gh-editor-subhead {
display: block;
width: 100%;
max-width: unset;
min-width: auto;
margin: 0;
padding: 0;
border: none;
background: transparent;
color: var(--darkgrey);
font-size: 1.9rem;
font-weight: 440;
line-height: 1.4em;
letter-spacing: -.018em;
overflow: hidden;
box-shadow: none;
}
.gh-editor-subhead:focus {
box-shadow: none !important;
border: none !important;
}
.gh-editor-title-divider {
margin: 1.6rem 0 4.8rem;
}
.gh-editor .tk-indicator { .gh-editor .tk-indicator {
position: absolute; position: absolute;
top: 15px; top: 15px;
@ -927,21 +1001,6 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone {
} }
} }
@media (max-width: 500px) {
.gh-editor-title {
font-size: 3.4rem;
}
}
.gh-editor-title {
padding: 0 0 4px;
}
.gh-editor-title::placeholder {
color: var(--lightgrey-d1);
font-weight: 700;
opacity: 1;
}
.gh-editor .editor-preview { .gh-editor .editor-preview {
height: auto; height: auto;
margin-top: 4px; margin-top: 4px;
@ -1168,61 +1227,6 @@ figure {
/* Labs /* Labs
/* ---------------------------------------------------------- */ /* ---------------------------------------------------------- */
.gh-editor-title-container {
position: relative;
max-width: 740px;
width: 100%;
margin-right: auto;
margin-left: auto;
border: none;
background: transparent;
}
.gh-editor .gh-editor-title {
display: block;
width: 100%;
max-width: unset;
min-height: auto;
margin: 0 0 1.2rem;
border: none;
background: transparent;
color: var(--black);
font-size: 4.8rem;
letter-spacing: -0.017em;
line-height: 1.1em;
font-weight: 700;
overflow: hidden;
box-shadow: none;
}
@media (min-width: 500px) and (max-width: 768px) {
.gh-editor .gh-editor-title {
font-size: 3.6rem;
}
}
@media (max-width: 500px) {
.gh-editor .gh-editor-title {
font-size: 2.8rem;
}
}
.gh-editor-hidden-indicator {
position: absolute;
top: -1px;
height: 2.4rem;
margin-left: -6rem;
color: var(--midgrey-l2);
}
.gh-editor-title-container .gh-editor-hidden-indicator {
top: 1.8rem;
}
.gh-editor-hidden-indicator svg {
height: 2.4rem;
}
.gh-setting-error { .gh-setting-error {
margin-top: 1em; margin-top: 1em;
line-height: 1.3em; line-height: 1.3em;

View File

@ -199,3 +199,7 @@
.gh-post-history-hidden-lexical { .gh-post-history-hidden-lexical {
display: none; display: none;
} }
.gh-post-history .gh-editor-feature-image p {
margin: 0 0 1.2rem;
}

View File

@ -61,10 +61,12 @@
--}} --}}
<GhKoenigEditorLexical <GhKoenigEditorLexical
@title={{readonly this.post.titleScratch}} @title={{readonly this.post.titleScratch}}
@excerpt={{readonly this.post.customExcerpt}}
@titleAutofocus={{this.shouldFocusTitle}} @titleAutofocus={{this.shouldFocusTitle}}
@titlePlaceholder={{concat (capitalize this.post.displayName) " title"}} @titlePlaceholder={{concat (capitalize this.post.displayName) " title"}}
@titleHasTk={{this.titleHasTk}} @titleHasTk={{this.titleHasTk}}
@onTitleChange={{this.updateTitleScratch}} @onTitleChange={{this.updateTitleScratch}}
@onExcerptChange={{this.updateExcerptScratch}}
@onTitleBlur={{perform this.saveTitleTask}} @onTitleBlur={{perform this.saveTitleTask}}
@body={{readonly this.post.lexicalScratch}} @body={{readonly this.post.lexicalScratch}}
@bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}} @bodyPlaceholder={{concat "Begin writing your " this.post.displayName "..."}}

View File

@ -51,7 +51,8 @@ const ALPHA_FEATURES = [
'tipsAndDonations', 'tipsAndDonations',
'importMemberTier', 'importMemberTier',
'lexicalIndicators', 'lexicalIndicators',
'adminXDemo' 'adminXDemo',
'subhead'
]; ];
module.exports.GA_KEYS = [...GA_FEATURES]; module.exports.GA_KEYS = [...GA_FEATURES];