From 328bd89d8dc96c81da0a50cc607995ca64e324df Mon Sep 17 00:00:00 2001 From: Ilya Y <75578537+ilya-bkv@users.noreply.github.com> Date: Tue, 22 Aug 2023 16:37:54 +0300 Subject: [PATCH] Article Lead and Description with simple editor (#189) * Article Lead and Description --- src/components/Article/Article.module.scss | 11 ++ src/components/Article/FullArticle.tsx | 3 + src/components/Editor/Editor.tsx | 2 +- .../Editor/SimplifiedEditor.module.scss | 45 +++++ src/components/Editor/SimplifiedEditor.tsx | 182 ++++++++++++------ src/components/Feed/ArticleCard.module.scss | 6 +- src/components/Feed/ArticleCard.tsx | 8 +- src/components/Views/Edit.tsx | 23 +-- .../Views/PublishSettings/PublishSettings.tsx | 27 +-- .../GrowingTextarea.module.scss | 2 +- src/context/editor.tsx | 5 +- src/graphql/mutation/article-update.ts | 2 + src/graphql/query/article-load.ts | 2 + src/graphql/query/articles-load-by.ts | 2 + src/graphql/types.gen.ts | 3 + 15 files changed, 221 insertions(+), 102 deletions(-) diff --git a/src/components/Article/Article.module.scss b/src/components/Article/Article.module.scss index 8b5a1405..650ede1a 100644 --- a/src/components/Article/Article.module.scss +++ b/src/components/Article/Article.module.scss @@ -577,3 +577,14 @@ a[data-toggle='tooltip'] { border-color: var(--black-500) transparent transparent transparent; } } + +.lead { + @include font-size(1.8rem); + + font-weight: 600; + + b, + strong { + font-weight: 700; + } +} diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index e972bc5d..f8812e8d 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -233,6 +233,9 @@ export const FullArticle = (props: Props) => { + +
+ { Image, Figcaption, Embed, - CharacterCount, + CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 BubbleMenu.configure({ pluginKey: 'textBubbleMenu', element: textBubbleMenuRef.current, diff --git a/src/components/Editor/SimplifiedEditor.module.scss b/src/components/Editor/SimplifiedEditor.module.scss index 7f15041b..7f774dc9 100644 --- a/src/components/Editor/SimplifiedEditor.module.scss +++ b/src/components/Editor/SimplifiedEditor.module.scss @@ -5,6 +5,7 @@ background: var(--black-50); border-radius: 16px; padding: 16px 16px 8px; + position: relative; .simplifiedEditorField { @include font-size(1.4rem); @@ -92,4 +93,48 @@ bottom: 0; } } + + &.minimal { + background: unset; + padding: 0; + + & div[contenteditable] { + font-size: 1.6rem; + font-weight: 500; + } + } + + &.bordered { + box-sizing: border-box; + padding: 16px 12px 6px 12px; + border-radius: 2px; + border: 2px solid var(--black-100); + background: var(--white-500); + + & div[contenteditable] { + font-size: 1.6rem; + font-weight: 500; + } + } + + &.labelVisible { + padding-top: 22px; + } + + .limit { + position: absolute; + right: 1rem; + bottom: 0.5rem; + font-weight: 500; + font-size: 1.2rem; + } + + .label { + @include font-size(1.2rem); + + position: absolute; + top: 6px; + left: 12px; + color: var(--black-400); + } } diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 30de5cc4..ec6b0ac9 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -1,4 +1,4 @@ -import { createEffect, onCleanup, onMount, Show } from 'solid-js' +import { createEffect, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createEditorTransaction, createTiptapEditor, @@ -30,12 +30,19 @@ import { UploadedFile } from '../../pages/types' import { Figure } from './extensions/Figure' import { Image } from '@tiptap/extension-image' import { Figcaption } from './extensions/Figcaption' +import { TextBubbleMenu } from './TextBubbleMenu' +import { BubbleMenu } from '@tiptap/extension-bubble-menu' +import { CharacterCount } from '@tiptap/extension-character-count' +import { createStore } from 'solid-js/store' type Props = { initialContent?: string + label?: string onSubmit?: (text: string) => void onChange?: (text: string) => void placeholder: string + variant?: 'minimal' | 'bordered' + maxLength?: number submitButtonText?: string quoteEnabled?: boolean imageEnabled?: boolean @@ -43,10 +50,13 @@ type Props = { smallHeight?: boolean submitByEnter?: boolean submitByShiftEnter?: boolean + onlyBubbleControls?: boolean } +export const MAX_DESCRIPTION_LIMIT = 400 const SimplifiedEditor = (props: Props) => { const { t } = useLocalize() + const [counter, setCounter] = createSignal() const wrapperEditorElRef: { current: HTMLElement @@ -60,6 +70,12 @@ const SimplifiedEditor = (props: Props) => { current: null } + const textBubbleMenuRef: { + current: HTMLDivElement + } = { + current: null + } + const { actions: { setEditor } } = useEditorContext() @@ -69,6 +85,7 @@ const SimplifiedEditor = (props: Props) => { content: 'figcaption image' }) + const content = props.initialContent const editor = createTiptapEditor(() => ({ element: editorElRef.current, editorProps: { @@ -85,11 +102,25 @@ const SimplifiedEditor = (props: Props) => { Link.configure({ openOnClick: false }), + + CharacterCount.configure({ + limit: MAX_DESCRIPTION_LIMIT + }), Blockquote.configure({ HTMLAttributes: { class: styles.blockQuote } }), + BubbleMenu.configure({ + pluginKey: 'textBubbleMenu', + element: textBubbleMenuRef.current, + shouldShow: ({ view, state }) => { + if (!props.onlyBubbleControls) return + const { selection } = state + const { empty } = selection + return view.hasFocus() && !empty + } + }), ImageFigure, Image, Figcaption, @@ -98,7 +129,7 @@ const SimplifiedEditor = (props: Props) => { placeholder: props.placeholder }) ], - content: props.initialContent ?? null + content: content ?? null })) setEditor(editor) @@ -193,94 +224,110 @@ const SimplifiedEditor = (props: Props) => { const handleInsertLink = () => !editor().state.selection.empty && showModal('editorInsertLink') + createEffect(() => { + if (html()) { + setCounter(editor().storage.characterCount.characters()) + } + }) return (
(wrapperEditorElRef.current = el)} class={clsx(styles.SimplifiedEditor, { [styles.smallHeight]: props.smallHeight, - [styles.isFocused]: isFocused() || !isEmpty() + [styles.minimal]: props.variant === 'minimal', + [styles.bordered]: props.variant === 'bordered', + [styles.isFocused]: isFocused() || !isEmpty(), + [styles.labelVisible]: props.label && counter() > 0 })} > + +
{MAX_DESCRIPTION_LIMIT - counter()}
+
+ 0}> +
{props.label}
+
(editorElRef.current = el)} /> -
-
- - {(triggerRef: (el) => void) => ( - - )} - - - {(triggerRef: (el) => void) => ( - - )} - - - {(triggerRef: (el) => void) => ( - - )} - - - + +
+
+ {(triggerRef: (el) => void) => ( )} - - - + {(triggerRef: (el) => void) => ( )} + + {(triggerRef: (el) => void) => ( + + )} + + + + {(triggerRef: (el) => void) => ( + + )} + + + + + {(triggerRef: (el) => void) => ( + + )} + + +
+ +
+
- -
-
-
-
+ hideModal()} /> @@ -293,6 +340,13 @@ const SimplifiedEditor = (props: Props) => { /> + + (textBubbleMenuRef.current = el)} + /> +
) } diff --git a/src/components/Feed/ArticleCard.module.scss b/src/components/Feed/ArticleCard.module.scss index b61190bf..20343da5 100644 --- a/src/components/Feed/ArticleCard.module.scss +++ b/src/components/Feed/ArticleCard.module.scss @@ -180,12 +180,10 @@ } } -.shoutCardLead { +.shoutCardDescription { @include font-size(1.6rem); - color: var(--secondary-color); - font-weight: 400; - line-height: 1.3; + color: var(--default-color); margin-bottom: 1.4rem; } diff --git a/src/components/Feed/ArticleCard.tsx b/src/components/Feed/ArticleCard.tsx index 9c32b6c6..732bfb19 100644 --- a/src/components/Feed/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard.tsx @@ -164,10 +164,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
- - -
{props.article.lead}
-
@@ -196,7 +192,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
- + +
+
diff --git a/src/components/Views/Edit.tsx b/src/components/Views/Edit.tsx index 4658f545..8461eaa2 100644 --- a/src/components/Views/Edit.tsx +++ b/src/components/Views/Edit.tsx @@ -21,13 +21,13 @@ import deepEqual from 'fast-deep-equal' import { AutoSaveNotice } from '../Editor/AutoSaveNotice' import { PublishSettings } from './PublishSettings' import { createStore } from 'solid-js/store' +import SimplifiedEditor from '../Editor/SimplifiedEditor' type Props = { shout: Shout } export const MAX_HEADER_LIMIT = 100 -export const MAX_LEAD_LIMIT = 400 export const EMPTY_TOPIC: Topic = { id: -1, slug: '' @@ -64,6 +64,8 @@ export const EditView = (props: Props) => { slug: props.shout.slug, shoutId: props.shout.id, title: props.shout.title, + lead: props.shout.lead, + description: props.shout.description, subtitle: props.shout.subtitle, selectedTopics: shoutTopics, mainTopic: shoutTopics.find((topic) => topic.slug === props.shout.mainTopic) || EMPTY_TOPIC, @@ -75,7 +77,6 @@ export const EditView = (props: Props) => { } const subtitleInput: { current: HTMLTextAreaElement } = { current: null } - const leadInput: { current: HTMLTextAreaElement } = { current: null } const [prevForm, setPrevForm] = createStore(clone(form)) const [saving, setSaving] = createSignal(false) @@ -226,7 +227,6 @@ export const EditView = (props: Props) => { } const showLeadInput = () => { setIsLeadVisible(true) - leadInput.current.focus() } return ( @@ -320,16 +320,13 @@ export const EditView = (props: Props) => { /> - { - leadInput.current = el - }} - allowEnterKey={true} - value={(value) => setForm('lead', value)} - class={styles.leadInput} - placeholder={t('Description')} - initialValue={form.subtitle} - maxLength={MAX_LEAD_LIMIT} + setForm('lead', value)} /> diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 287b67f5..baa4e5ce 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -10,7 +10,7 @@ import { useLocalize } from '../../../context/localize' import { Modal } from '../../Nav/Modal' import { Topic } from '../../../graphql/types.gen' import { apiClient } from '../../../utils/apiClient' -import { EMPTY_TOPIC, MAX_LEAD_LIMIT } from '../Edit' +import { EMPTY_TOPIC } from '../Edit' import { useSession } from '../../../context/session' import { Icon } from '../../_shared/Icon' import stylesBeside from '../../Feed/Beside.module.scss' @@ -19,6 +19,7 @@ import { router } from '../../../stores/router' import { GrowingTextarea } from '../../_shared/GrowingTextarea' import { createStore } from 'solid-js/store' import { UploadedFile } from '../../../pages/types' +import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor' type Props = { shoutId: number @@ -35,12 +36,12 @@ export const PublishSettings = (props: Props) => { const { t } = useLocalize() const { user } = useSession() - const composeLead = () => { - if (!props.form.lead) { + const composeDescription = () => { + if (!props.form.description) { const leadText = props.form.body.replaceAll(/<\/?[^>]+(>|$)/gi, ' ') - return shorten(leadText, MAX_LEAD_LIMIT).trim() + return shorten(leadText, MAX_DESCRIPTION_LIMIT).trim() } - return props.form.lead + return props.form.description } const initialData: Partial = { @@ -49,7 +50,7 @@ export const PublishSettings = (props: Props) => { slug: props.form.slug, title: props.form.title, subtitle: props.form.subtitle, - lead: composeLead() + description: composeDescription() } const { @@ -183,15 +184,15 @@ export const PublishSettings = (props: Props) => { allowEnterKey={false} maxLength={100} /> - setSettingsForm('lead', value)} - allowEnterKey={false} - maxLength={MAX_LEAD_LIMIT} + label={t('Description')} + initialContent={composeDescription()} + onChange={(value) => setForm('description', value)} + maxLength={MAX_DESCRIPTION_LIMIT} />
diff --git a/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss b/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss index 967f3d1d..db819c9e 100644 --- a/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss +++ b/src/components/_shared/GrowingTextarea/GrowingTextarea.module.scss @@ -9,7 +9,7 @@ padding: 16px 12px; border-radius: 2px; border: 2px solid var(--black-100); - background: var(--white-500, #fff); + background: var(--white-500); } &.hasFieldName { diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 28316a7d..d65080c2 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -21,12 +21,13 @@ export type ShoutForm = { slug: string title: string subtitle: string + lead?: string + description?: string selectedTopics: Topic[] mainTopic?: Topic body: string coverImageUrl: string media?: string - lead?: string } type EditorContextType = { @@ -136,6 +137,8 @@ export const EditorProvider = (props: { children: JSX.Element }) => { slug: formToUpdate.slug, subtitle: formToUpdate.subtitle, title: formToUpdate.title, + lead: formToUpdate.lead, + description: formToUpdate.description, cover: formToUpdate.coverImageUrl, media: formToUpdate.media }, diff --git a/src/graphql/mutation/article-update.ts b/src/graphql/mutation/article-update.ts index da53afe7..a41c5c1a 100644 --- a/src/graphql/mutation/article-update.ts +++ b/src/graphql/mutation/article-update.ts @@ -9,6 +9,8 @@ export default gql` slug title subtitle + lead + description body visibility } diff --git a/src/graphql/query/article-load.ts b/src/graphql/query/article-load.ts index 1fac1b12..d16589ba 100644 --- a/src/graphql/query/article-load.ts +++ b/src/graphql/query/article-load.ts @@ -5,6 +5,8 @@ export default gql` loadShout(slug: $slug, shout_id: $shoutId) { id title + lead + description visibility subtitle slug diff --git a/src/graphql/query/articles-load-by.ts b/src/graphql/query/articles-load-by.ts index 9e8dd616..5db2edca 100644 --- a/src/graphql/query/articles-load-by.ts +++ b/src/graphql/query/articles-load-by.ts @@ -5,6 +5,8 @@ export default gql` loadShouts(options: $options) { id title + lead + description subtitle slug layout diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index ccf57ba9..61516a6b 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -554,6 +554,7 @@ export type Shout = { createdAt: Scalars['DateTime'] deletedAt?: Maybe deletedBy?: Maybe + description?: Maybe id: Scalars['Int'] lang?: Maybe layout?: Maybe @@ -577,7 +578,9 @@ export type ShoutInput = { body?: InputMaybe community?: InputMaybe cover?: InputMaybe + description?: InputMaybe layout?: InputMaybe + lead?: InputMaybe mainTopic?: InputMaybe media?: InputMaybe slug?: InputMaybe