From 2fad5b8db94de8021f664b44e223a77d3aa5166c Mon Sep 17 00:00:00 2001 From: Untone Date: Sun, 15 Sep 2024 21:47:21 +0300 Subject: [PATCH] bad update 2 --- .../Editor/MiniEditor/MiniEditor.stories.tsx | 64 ++++++ .../Editor/MiniEditor/MiniEditor.tsx | 191 ++++++++++++++++++ .../Feed/ArticleCard/ArticleCard.tsx | 6 +- .../FeedArticlePopup/FeedArticlePopup.tsx | 11 +- src/components/Topic/Full.tsx | 4 +- .../Views/AllAuthors/AllAuthors.tsx | 2 +- .../Views/Profile/ProfileSecurity.tsx | 2 +- .../Views/Profile/ProfileSettings.tsx | 55 +++-- src/context/following.tsx | 21 +- src/lib/editorOptions.ts | 103 ++++++++++ 10 files changed, 404 insertions(+), 55 deletions(-) create mode 100644 src/components/Editor/MiniEditor/MiniEditor.stories.tsx create mode 100644 src/components/Editor/MiniEditor/MiniEditor.tsx create mode 100644 src/lib/editorOptions.ts diff --git a/src/components/Editor/MiniEditor/MiniEditor.stories.tsx b/src/components/Editor/MiniEditor/MiniEditor.stories.tsx new file mode 100644 index 00000000..80e227de --- /dev/null +++ b/src/components/Editor/MiniEditor/MiniEditor.stories.tsx @@ -0,0 +1,64 @@ +import { Meta, StoryObj } from 'storybook-solidjs' +import MiniEditor from './MiniEditor' + +const meta: Meta = { + title: 'Components/MiniEditor', + component: MiniEditor, + argTypes: { + content: { + control: 'text', + description: 'Initial content for the editor', + defaultValue: '' + }, + limit: { + control: 'number', + description: 'Character limit for the editor', + defaultValue: 500 + }, + placeholder: { + control: 'text', + description: 'Placeholder text when the editor is empty', + defaultValue: 'Start typing here...' + }, + onChange: { + action: 'changed', + description: 'Callback when the content changes' + } + } +} + +export default meta + +type Story = StoryObj + +export const Default: Story = { + args: { + content: '', + limit: 500, + placeholder: 'Start typing here...' + } +} + +export const WithInitialContent: Story = { + args: { + content: 'This is some initial content', + limit: 500, + placeholder: 'Start typing here...' + } +} + +export const WithCharacterLimit: Story = { + args: { + content: '', + limit: 50, + placeholder: 'You have a 50 character limit...' + } +} + +export const WithCustomPlaceholder: Story = { + args: { + content: '', + limit: 500, + placeholder: 'Custom placeholder here...' + } +} diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx new file mode 100644 index 00000000..26c86482 --- /dev/null +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -0,0 +1,191 @@ +import type { Editor } from '@tiptap/core' +import CharacterCount from '@tiptap/extension-character-count' +import Placeholder from '@tiptap/extension-placeholder' +import clsx from 'clsx' +import { type JSX, Show, createEffect, createSignal, onCleanup } from 'solid-js' +import { + createEditorTransaction, + createTiptapEditor, + useEditorHTML, + useEditorIsEmpty, + useEditorIsFocused +} from 'solid-tiptap' +import { Toolbar } from 'terracotta' + +import { useLocalize } from '~/context/localize' +import { useUI } from '~/context/ui' +import { base, custom } from '~/lib/editorOptions' +import { Icon } from '../_shared/Icon/Icon' +import { Popover } from '../_shared/Popover/Popover' +import { InsertLinkForm } from './InsertLinkForm/InsertLinkForm' + +import styles from './SimplifiedEditor.module.scss' + +interface ControlProps { + editor: Editor + title: string + key: string + onChange: () => void + isActive?: (editor: Editor) => boolean + children: JSX.Element +} + +function Control(props: ControlProps): JSX.Element { + const handleClick = (ev?: MouseEvent) => { + ev?.preventDefault() + ev?.stopPropagation() + props.onChange?.() + } + + return ( + + {(triggerRef: (el: HTMLElement) => void) => ( + + )} + + ) +} + +interface MiniEditorProps { + content?: string + onChange?: (content: string) => void + limit?: number + placeholder?: string +} + +export default function MiniEditor(props: MiniEditorProps): JSX.Element { + const [editorElement, setEditorElement] = createSignal() + const [counter, setCounter] = createSignal(0) + const [showLinkInput, setShowLinkInput] = createSignal(false) + const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) + const { t } = useLocalize() + const { showModal } = useUI() + + const editor = createTiptapEditor(() => ({ + element: editorElement()!, + extensions: [ + ...base, + ...custom, + Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }), + CharacterCount.configure({ limit: props.limit }) + ], + editorProps: { + attributes: { + class: styles.simplifiedEditorField + } + }, + content: props.content || '' + })) + + const isEmpty = useEditorIsEmpty(editor) + const isFocused = useEditorIsFocused(editor) + const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty) + const html = useEditorHTML(editor) + + createEffect(() => setShowSimpleMenu(isTextSelection())) + + createEffect(() => { + const textLength = editor()?.getText().length || 0 + setCounter(textLength) + const content = html() + content && props.onChange?.(content) + }) + + const handleLinkClick = () => { + setShowLinkInput(!showLinkInput()) + editor()?.chain().focus().run() + } + + // Prevent focus loss when clicking inside the toolbar + const handleMouseDownOnToolbar = (event: MouseEvent) => { + event.preventDefault() // Prevent the default focus shift + } + const [toolbarElement, setToolbarElement] = createSignal() + // Attach the event handler to the toolbar + onCleanup(() => { + toolbarElement()?.removeEventListener('mousedown', handleMouseDownOnToolbar) + }) + return ( +
+
+ + + + {(instance) => ( +
+ setShowLinkInput(false)} />} + > +
+ instance.chain().focus().toggleBold().run()} + title={t('Bold')} + > + + + instance.chain().focus().toggleItalic().run()} + title={t('Italic')} + > + + + + + + instance.chain().focus().toggleBlockquote().run()} + title={t('Add blockquote')} + > + + + showModal('simplifiedEditorUploadImage')} + title={t('Add image')} + > + + +
+
+
+ )} +
+
+
+ +
+ + 0}> + + {counter()} / {props.limit || '∞'} + + +
+
+ ) +} diff --git a/src/components/Feed/ArticleCard/ArticleCard.tsx b/src/components/Feed/ArticleCard/ArticleCard.tsx index 635fe7d2..28e953f6 100644 --- a/src/components/Feed/ArticleCard/ArticleCard.tsx +++ b/src/components/Feed/ArticleCard/ArticleCard.tsx @@ -55,7 +55,7 @@ const desktopCoverImageWidths: Record = { M: 600, L: 800 } - +const titleSeparator = /{!|\?|:|;}\s/ const getTitleAndSubtitle = ( article: Shout ): { @@ -69,7 +69,7 @@ const getTitleAndSubtitle = ( let titleParts = article.title?.split('. ') || [] if (titleParts?.length === 1) { - titleParts = article.title?.split(/{!|\?|:|;}\s/) || [] + titleParts = article.title?.split(titleSeparator) || [] } if (titleParts && titleParts.length > 1) { @@ -88,7 +88,7 @@ const getMainTopicTitle = (article: Shout, lng: string) => { const mainTopicSlug = article.main_topic || '' const mainTopic = (article.topics || []).find((tpc: Maybe) => tpc?.slug === mainTopicSlug) const mainTopicTitle = - mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || '' + mainTopicSlug && lng === 'en' ? mainTopicSlug.replaceAll('-', ' ') : mainTopic?.title || '' return [mainTopicTitle, mainTopicSlug] } diff --git a/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx b/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx index 1aaf587b..d155780b 100644 --- a/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx +++ b/src/components/Feed/FeedArticlePopup/FeedArticlePopup.tsx @@ -33,7 +33,6 @@ export const FeedArticlePopup = (props: Props) => {
  • -
  • - )} - */} +
  • @@ -284,18 +282,20 @@ export const ProfileSettings = () => { )}

    - updateFormField('name', event.currentTarget.value)} - value={form.name || ''} - ref={(el) => (nameInputRef = el)} - /> - +
    { />

    {t('About')}

    - updateFormField('about', value)} + placeholder={t('About')} />
    diff --git a/src/context/following.tsx b/src/context/following.tsx index 69425d1c..f4eb4853 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -151,16 +151,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { setFollows((prevFollows: AuthorFollowsResult) => { const updatedFollows = { ...prevFollows } switch (what) { - case 'AUTHOR': { - if (value) { - if (!updatedFollows.authors?.some((author) => author.slug === slug)) { - updatedFollows.authors = [...(updatedFollows.authors || []), { slug } as Author] - } - } else { - updatedFollows.authors = updatedFollows.authors?.filter((author) => author.slug !== slug) || [] - } - break - } case 'TOPIC': { if (value) { if (!updatedFollows.topics?.some((topic) => topic.slug === slug)) { @@ -182,6 +172,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { } break } + // case 'AUTHOR': + default: { + if (value) { + if (!updatedFollows.authors?.some((author) => author.slug === slug)) { + updatedFollows.authors = [...(updatedFollows.authors || []), { slug } as Author] + } + } else { + updatedFollows.authors = updatedFollows.authors?.filter((author) => author.slug !== slug) || [] + } + break + } } return updatedFollows }) diff --git a/src/lib/editorOptions.ts b/src/lib/editorOptions.ts new file mode 100644 index 00000000..065de790 --- /dev/null +++ b/src/lib/editorOptions.ts @@ -0,0 +1,103 @@ +import { EditorOptions } from '@tiptap/core' +import Highlight from '@tiptap/extension-highlight' +import Image from '@tiptap/extension-image' +import Link from '@tiptap/extension-link' +import Underline from '@tiptap/extension-underline' +import StarterKit from '@tiptap/starter-kit' +import { CustomBlockquote } from '~/components/Editor/extensions/CustomBlockquote' +import { Figcaption } from '~/components/Editor/extensions/Figcaption' +import { Figure } from '~/components/Editor/extensions/Figure' +import { Footnote } from '~/components/Editor/extensions/Footnote' +import { Iframe } from '~/components/Editor/extensions/Iframe' +import { Span } from '~/components/Editor/extensions/Span' +import { ToggleTextWrap } from '~/components/Editor/extensions/ToggleTextWrap' +import { TrailingNode } from '~/components/Editor/extensions/TrailingNode' + +// Extend the Figure extension to include Figcaption +const ImageFigure = Figure.extend({ + name: 'capturedImage', + content: 'figcaption image' +}) + +export const base: EditorOptions['extensions'] = [ + StarterKit.configure({ + heading: { + levels: [2, 3, 4] + }, + horizontalRule: { + HTMLAttributes: { + class: 'horizontalRule' + } + }, + blockquote: undefined + }), + Underline, // не входит в StarterKit + Link.configure({ + autolink: true, + openOnClick: false + }), + Image, + Highlight.configure({ + multicolor: true, + HTMLAttributes: { + class: 'highlight' + } + }) +] + +export const custom: EditorOptions['extensions'] = [ + ImageFigure, + Figure, + Figcaption, + Footnote, + CustomBlockquote, + Iframe, + Span, + ToggleTextWrap, + TrailingNode + // Добавьте другие кастомные расширения здесь +] + +export const collab: EditorOptions['extensions'] = [] +/* + content: '', + autofocus: false, + editable: false, + element: undefined, + injectCSS: false, + injectNonce: undefined, + editorProps: {} as EditorProps, + parseOptions: {} as EditorOptions['parseOptions'], + enableInputRules: false, + enablePasteRules: false, + enableCoreExtensions: false, + enableContentCheck: false, + onBeforeCreate: (_props: EditorEvents['beforeCreate']): void => { + throw new Error('Function not implemented.') + }, + onCreate: (_props: EditorEvents['create']): void => { + throw new Error('Function not implemented.') + }, + onContentError: (_props: EditorEvents['contentError']): void => { + throw new Error('Function not implemented.') + }, + onUpdate: (_props: EditorEvents['update']): void => { + throw new Error('Function not implemented.') + }, + onSelectionUpdate: (_props: EditorEvents['selectionUpdate']): void => { + throw new Error('Function not implemented.') + }, + onTransaction: (_props: EditorEvents['transaction']): void => { + throw new Error('Function not implemented.') + }, + onFocus: (_props: EditorEvents['focus']): void => { + throw new Error('Function not implemented.') + }, + onBlur: (_props: EditorEvents['blur']): void => { + throw new Error('Function not implemented.') + }, + onDestroy: (_props: EditorEvents['destroy']): void => { + throw new Error('Function not implemented.') + } +} +*/