From b3b8e51d2dfe2bd56dc5b024ab6fa0eb19b6fcf6 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 06:50:44 +0300 Subject: [PATCH 01/23] devruntime-fix --- app.config.ts | 10 +++++++--- package-lock.json | 42 +++++++++++++++++++++--------------------- package.json | 8 ++++---- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/app.config.ts b/app.config.ts index fd80e5f4..d51d23b1 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,12 +1,16 @@ import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config' +import dotenv from 'dotenv' import viteConfig from './vite.config' -const isVercel = Boolean(process?.env.VERCEL) -const isNetlify = Boolean(process?.env.NETLIFY) +// Load environment variables from .env file +dotenv.config() + +const isVercel = Boolean(process.env.VERCEL) +const isNetlify = Boolean(process.env.NETLIFY) const isBun = Boolean(process.env.BUN) export const runtime = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node' -console.info(`[app.config] solid-start build for ${runtime}!`) +console.info(`[app.config] solid-start build for {> ${runtime} <}`) export default defineConfig({ nitro: { diff --git a/package-lock.json b/package-lock.json index 8eb710dd..bc0e0174 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,7 +77,7 @@ "@tiptap/starter-kit": "^2.7.2", "@types/cookie": "^0.6.0", "@types/cookie-signature": "^1.1.2", - "@types/node": "^22.5.5", + "@types/node": "^22.6.0", "@types/throttle-debounce": "^5.0.2", "@urql/core": "^5.0.6", "axe-playwright": "^2.0.2", @@ -100,7 +100,7 @@ "prosemirror-view": "^1.34.3", "rollup-plugin-visualizer": "^5.12.0", "sass": "1.77.6", - "solid-js": "^1.8.22", + "solid-js": "^1.8.23", "solid-popper": "^0.3.0", "solid-tiptap": "0.7.0", "solid-transition-group": "^0.2.3", @@ -124,7 +124,7 @@ "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-sass-dts": "^1.3.29", "y-prosemirror": "1.2.12", - "yjs": "13.6.18" + "yjs": "13.6.19" }, "engines": { "node": ">= 20" @@ -5254,9 +5254,9 @@ } }, "node_modules/@rollup/plugin-node-resolve": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.4.tgz", - "integrity": "sha512-wnKAGisav1m2vgVK2/2mNowK5DCqff7kpz76cY1pECVE0qRQTCAIcWP5xmdGDi8X8K9SYeeC98i6cD3fk6qkDg==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.0.tgz", + "integrity": "sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==", "dev": true, "license": "MIT", "dependencies": { @@ -5324,9 +5324,9 @@ } }, "node_modules/@rollup/pluginutils": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.1.tgz", - "integrity": "sha512-bVRmQqBIyGD+VMihdEV2IBurfIrdW9tD9yzJUL3CBRDbyPBVzQnBSMSgyUZHl1E335rpMRj7r4o683fXLYw8iw==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", "dev": true, "license": "MIT", "dependencies": { @@ -7740,9 +7740,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "version": "4.17.9", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.9.tgz", + "integrity": "sha512-w9iWudx1XWOHW5lQRS9iKpK/XuRhnN+0T7HvdCCd802FYkT1AMTnxndJHGrNJwRoRHkslGr4S29tjm1cT7x/7w==", "dev": true, "license": "MIT" }, @@ -7771,9 +7771,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.5.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.5.tgz", - "integrity": "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA==", + "version": "22.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.6.1.tgz", + "integrity": "sha512-V48tCfcKb/e6cVUigLAaJDAILdMP0fUW6BidkPK4GpGjXcfbnoHasCZDwz3N3yVt5we2RHm4XTQCpv0KJz9zqw==", "dev": true, "license": "MIT", "dependencies": { @@ -22932,9 +22932,9 @@ } }, "node_modules/solid-js": { - "version": "1.8.22", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.22.tgz", - "integrity": "sha512-VBzN5j+9Y4rqIKEnK301aBk+S7fvFSTs9ljg+YEdFxjNjH0hkjXPiQRcws9tE5fUzMznSS6KToL5hwMfHDgpLA==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.23.tgz", + "integrity": "sha512-0jKzMgxmU/b3k4iJmIZJW2BIArrHN+Mug0n7m7MeHvGHWiS57ZdyTmnqNMSbGRvE73QBnTiGFJc90cPPieawaA==", "dev": true, "license": "MIT", "dependencies": { @@ -26804,9 +26804,9 @@ } }, "node_modules/yjs": { - "version": "13.6.18", - "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.18.tgz", - "integrity": "sha512-GBTjO4QCmv2HFKFkYIJl7U77hIB1o22vSCSQD1Ge8ZxWbIbn8AltI4gyXbtL+g5/GJep67HCMq3Y5AmNwDSyEg==", + "version": "13.6.19", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.19.tgz", + "integrity": "sha512-GNKw4mEUn5yWU2QPHRx8jppxmCm9KzbBhB4qJLUJFiiYD0g/tDVgXQ7aPkyh01YO28kbs2J/BEbWBagjuWyejw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4b9a9492..c34f9db0 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@tiptap/starter-kit": "^2.7.2", "@types/cookie": "^0.6.0", "@types/cookie-signature": "^1.1.2", - "@types/node": "^22.5.5", + "@types/node": "^22.6.0", "@types/throttle-debounce": "^5.0.2", "@urql/core": "^5.0.6", "axe-playwright": "^2.0.2", @@ -107,7 +107,7 @@ "prosemirror-view": "^1.34.3", "rollup-plugin-visualizer": "^5.12.0", "sass": "1.77.6", - "solid-js": "^1.8.22", + "solid-js": "^1.8.23", "solid-popper": "^0.3.0", "solid-tiptap": "0.7.0", "solid-transition-group": "^0.2.3", @@ -131,12 +131,12 @@ "vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-sass-dts": "^1.3.29", "y-prosemirror": "1.2.12", - "yjs": "13.6.18" + "yjs": "13.6.19" }, "overrides": { "sass": "1.77.6", "vite": "5.3.5", - "yjs": "13.6.18", + "yjs": "13.6.19", "y-prosemirror": "1.2.12" }, "engines": { From d7a5a188ff0a64a2331c679ce3c9ee239577a9d7 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 09:48:39 +0300 Subject: [PATCH 02/23] editor-showup+fixes --- .gitignore | 2 +- .storybook/test-runner.ts | 9 +++-- .stylelintignore | 6 ++- app.config.ts | 14 +++---- package.json | 2 +- src/components/Editor/Editor.tsx | 37 ++----------------- .../Editor/MiniEditor/MiniEditor.tsx | 2 +- src/components/Editor/SimplifiedEditor.tsx | 2 +- src/components/Views/EditView/EditView.tsx | 28 +++++++------- src/intl/chars.ts | 2 +- src/intl/translate.ts | 21 +++++++++++ .../{editorOptions.ts => editorExtensions.ts} | 18 ++++++++- vite.config.ts | 25 ++++++++----- 13 files changed, 91 insertions(+), 77 deletions(-) rename src/lib/{editorOptions.ts => editorExtensions.ts} (83%) diff --git a/.gitignore b/.gitignore index 1854c000..881888d4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,9 @@ bun.lockb /plawright-report/ target .github/dependabot.yml - .output .vinxi *.pem edge.* .vscode/settings.json +storybook-static diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 91a12ca8..7e913f5f 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -1,3 +1,4 @@ +import type { Page } from '@playwright/test' import type { TestRunnerConfig } from '@storybook/test-runner' import { checkA11y, injectAxe } from 'axe-playwright' @@ -5,11 +6,11 @@ import { checkA11y, injectAxe } from 'axe-playwright' * See https://storybook.js.org/docs/react/writing-tests/test-runner#test-hook-api-experimental * to learn more about the test-runner hooks API. */ -const a11yConfig: TestRunnerConfig = { - async preRender(page) { +const a11yConfig = { + async preRender(page: Page) { await injectAxe(page) }, - async postRender(page) { + async postRender(page: Page) { await checkA11y(page, '#storybook-root', { detailedReport: true, detailedReportOptions: { @@ -17,6 +18,6 @@ const a11yConfig: TestRunnerConfig = { } }) } -} +} as TestRunnerConfig module.exports = a11yConfig diff --git a/.stylelintignore b/.stylelintignore index fd2fbae2..f313728d 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1,6 @@ -.vercel/ +node_modules dist/ +storybook-static +.output +.vinxi +.vercel diff --git a/app.config.ts b/app.config.ts index d51d23b1..7b29f3ae 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,16 +1,12 @@ import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config' -import dotenv from 'dotenv' -import viteConfig from './vite.config' - -// Load environment variables from .env file -dotenv.config() +import viteConfig, { isDev } from './vite.config' const isVercel = Boolean(process.env.VERCEL) const isNetlify = Boolean(process.env.NETLIFY) const isBun = Boolean(process.env.BUN) -export const runtime = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node' -console.info(`[app.config] solid-start build for {> ${runtime} <}`) +const preset = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node' +console.info(`[app.config] solid-start preset {> ${preset} <}`) export default defineConfig({ nitro: { @@ -18,10 +14,10 @@ export default defineConfig({ }, ssr: true, server: { - preset: runtime, + preset, port: 3000, https: true }, - devOverlay: true, + devOverlay: isDev, vite: viteConfig } as SolidStartInlineConfig) diff --git a/package.json b/package.json index c34f9db0..09e86c9b 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@tiptap/starter-kit": "^2.7.2", "@types/cookie": "^0.6.0", "@types/cookie-signature": "^1.1.2", - "@types/node": "^22.6.0", + "@types/node": "^22.6.1", "@types/throttle-debounce": "^5.0.2", "@urql/core": "^5.0.6", "axe-playwright": "^2.0.2", diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 81d31163..04879023 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -4,14 +4,7 @@ import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { CharacterCount } from '@tiptap/extension-character-count' import { Collaboration } from '@tiptap/extension-collaboration' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' -import { Dropcursor } from '@tiptap/extension-dropcursor' import { FloatingMenu } from '@tiptap/extension-floating-menu' -import Focus from '@tiptap/extension-focus' -import { Gapcursor } from '@tiptap/extension-gapcursor' -import { HardBreak } from '@tiptap/extension-hard-break' -import { Highlight } from '@tiptap/extension-highlight' -import { HorizontalRule } from '@tiptap/extension-horizontal-rule' -import { Image } from '@tiptap/extension-image' import { Placeholder } from '@tiptap/extension-placeholder' import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js' import uniqolor from 'uniqolor' @@ -21,23 +14,14 @@ import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' import { Author } from '~/graphql/schema/core.gen' +import { base, custom, extended } from '~/lib/editorExtensions' import { handleImageUpload } from '~/lib/handleImageUpload' import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' import { TextBubbleMenu } from './TextBubbleMenu' -import { ArticleNode } from './extensions/Article' -import { CustomBlockquote } from './extensions/CustomBlockquote' -import { Figcaption } from './extensions/Figcaption' -import { Figure } from './extensions/Figure' -import { Footnote } from './extensions/Footnote' -import { Iframe } from './extensions/Iframe' -import { Span } from './extensions/Span' -import { ToggleTextWrap } from './extensions/ToggleTextWrap' -import { TrailingNode } from './extensions/TrailingNode' import { renderUploadedImage } from './renderUploadedImage' import './Prosemirror.scss' -import { base } from '~/lib/editorOptions' export type EditorComponentProps = { shoutId: number @@ -118,26 +102,11 @@ export const EditorComponent = (props: EditorComponentProps) => { }, extensions: [ ...base, + ...custom, + ...extended, - HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }), - Dropcursor, - CustomBlockquote, - Span, - ToggleTextWrap, Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }), - Focus, - Gapcursor, - HardBreak, - Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }), - Image, - Iframe, - Figure, - Figcaption, - Footnote, - ToggleTextWrap, CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 - TrailingNode, - ArticleNode, // menus diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index c866901f..011d951e 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -15,7 +15,7 @@ import { Icon } from '~/components/_shared/Icon/Icon' import { Popover } from '~/components/_shared/Popover/Popover' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' -import { base } from '~/lib/editorOptions' +import { base } from '~/lib/editorExtensions' import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' import styles from '../SimplifiedEditor.module.scss' diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 2f689753..a474f9c7 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -8,7 +8,7 @@ import { Portal } from 'solid-js/web' import { createEditorTransaction, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' import { useEditorContext } from '~/context/editor' import { useUI } from '~/context/ui' -import { base, custom } from '~/lib/editorOptions' +import { base, custom } from '~/lib/editorExtensions' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' import { UploadedFile } from '~/types/upload' import { Modal } from '../_shared/Modal/Modal' diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index db2a4edd..cfb1b4d3 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -1,7 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { - Accessor, Show, createEffect, createMemo, @@ -65,10 +64,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditView = (props: Props) => { const { t } = useLocalize() - const [isScrolled, setIsScrolled] = createSignal(false) const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - const { form, formErrors, @@ -78,14 +74,20 @@ export const EditView = (props: Props) => { saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext() - const [shoutTopics, setShoutTopics] = createSignal([]) - const [draft, setDraft] = createSignal() - let subtitleInput: HTMLTextAreaElement | null + + const [subtitleInput, setSubtitleInput] = createSignal() const [prevForm, setPrevForm] = createStore(clone(form)) const [saving, setSaving] = createSignal(false) const [isSubtitleVisible, setIsSubtitleVisible] = createSignal(Boolean(form.subtitle)) const [isLeadVisible, setIsLeadVisible] = createSignal(Boolean(form.lead)) - const mediaItems: Accessor = createMemo(() => JSON.parse(form.media || '[]')) + const [isScrolled, setIsScrolled] = createSignal(false) + const [shoutTopics, setShoutTopics] = createSignal([]) + const [draft, setDraft] = createSignal() + const [mediaItems, setMediaItems] = createSignal([]) + + const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + + createEffect(() => setMediaItems(JSON.parse(form.media || '[]'))) createEffect( on( @@ -97,7 +99,7 @@ export const EditView = (props: Props) => { const stored = getDraftFromLocalStorage(shout.id) if (stored) { // console.info(`[EditView] got stored shout: ${stored}`) - setDraft(stored) + setDraft((old) => ({...old, ...stored} as Shout)) } else { if (!shout.slug) { console.warn(`[EditView] shout has no slug! ${shout}`) @@ -131,7 +133,7 @@ export const EditView = (props: Props) => { (d) => { if (d) { const draftForm = Object.keys(d) ? d : { shoutId: props.shout.id } - setForm(draftForm) + setForm(draftForm as ShoutForm) console.debug('draft from localstorage: ', draftForm) } }, @@ -267,7 +269,7 @@ export const EditView = (props: Props) => { const showSubtitleInput = () => { setIsSubtitleVisible(true) - subtitleInput?.focus() + subtitleInput()?.focus() } const showLeadInput = () => { @@ -359,7 +361,7 @@ export const EditView = (props: Props) => { (subtitleInput = el)} + textAreaRef={setSubtitleInput} allowEnterKey={false} value={(value) => handleInputChange('subtitle', value || '')} class={styles.subtitleInput} @@ -455,7 +457,7 @@ export const EditView = (props: Props) => { - }> + }> { return cyrillicRegex.test(s) } +/** + * Translates the author's name based on the provided language. For English (`lng === 'en'`), it transliterates + * and capitalizes Cyrillic names, handling special cases for characters like 'ё' and 'ь'. + * @param author - The author object containing the name to translate. + * @param lng - The target language for translation ('en' or 'ru'). + * @returns The translated author name, or the original if no translation is needed. + */ export const translateAuthor = (author: Author, lng: string) => lng === 'en' && isCyrillic(author?.name || '') ? capitalize( @@ -15,6 +27,15 @@ export const translateAuthor = (author: Author, lng: string) => ) : author.name +/** + * Reduces a list of authors into groups based on the first readable letter of their last name. + * The grouping depends on the language ('ru' for Russian and 'en' for English). + * Non-Cyrillic or non-Latin characters are grouped under `@`. + * @param acc - The accumulator object for grouping authors by the first readable letter. + * @param author - The author object containing the name. + * @param lng - The language code ('en' or 'ru') used for transliteration and sorting. + * @returns The accumulator object with authors grouped by the first readable letter of their last name. + */ export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Author, lng: string) => { let letter = '' diff --git a/src/lib/editorOptions.ts b/src/lib/editorExtensions.ts similarity index 83% rename from src/lib/editorOptions.ts rename to src/lib/editorExtensions.ts index f7686d7f..b6603987 100644 --- a/src/lib/editorOptions.ts +++ b/src/lib/editorExtensions.ts @@ -1,9 +1,15 @@ import { EditorOptions } from '@tiptap/core' +import Dropcursor from '@tiptap/extension-dropcursor' +import Focus from '@tiptap/extension-focus' +import Gapcursor from '@tiptap/extension-gapcursor' +import HardBreak from '@tiptap/extension-hard-break' import Highlight from '@tiptap/extension-highlight' +import HorizontalRule from '@tiptap/extension-horizontal-rule' 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 ArticleNode from '~/components/Editor/extensions/Article' import { CustomBlockquote } from '~/components/Editor/extensions/CustomBlockquote' import { Figcaption } from '~/components/Editor/extensions/Figcaption' import { Figure } from '~/components/Editor/extensions/Figure' @@ -53,9 +59,17 @@ export const custom: EditorOptions['extensions'] = [ ] export const extended: EditorOptions['extensions'] = [ + HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }), + Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }), + Dropcursor, + CustomBlockquote, + Span, + ToggleTextWrap, Footnote, - CustomBlockquote - // TODO: Добавьте другие кастомные расширения здесь + Focus, + Gapcursor, + HardBreak, + ArticleNode ] /* diff --git a/vite.config.ts b/vite.config.ts index 5f3ee698..19278941 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,23 +1,23 @@ // biome-ignore lint/correctness/noNodejsModules: used during build import path from 'node:path' +// import { visualizer } from 'rollup-plugin-visualizer' +import dotenv from 'dotenv' import { CSSOptions } from 'vite' import mkcert from 'vite-plugin-mkcert' import { PolyfillOptions, nodePolyfills } from 'vite-plugin-node-polyfills' import sassDts from 'vite-plugin-sass-dts' -// import { visualizer } from 'rollup-plugin-visualizer' -const isDev = process.env.NODE_ENV !== 'production' -console.log(`[vite.config] development mode: ${isDev}`) +// Load environment variables from .env file +dotenv.config() + +export const isDev = process.env.NODE_ENV !== 'production' +console.log(`[vite.config] ${process.env.NODE_ENV} mode`) const polyfillOptions = { include: ['path', 'stream', 'util'], exclude: ['http'], - globals: { - Buffer: true - }, - overrides: { - fs: 'memfs' - }, + globals: { Buffer: true }, + overrides: { fs: 'memfs' }, protocolImports: true } as PolyfillOptions @@ -45,12 +45,19 @@ export default { build: { target: 'esnext', sourcemap: true, + minify: 'terser', // explicit terser usage + terserOptions: { + compress: { + drop_console: true // removes console logs in production + } + }, rollupOptions: { // plugins: [visualizer()] output: { manualChunks: { icons: ['./src/components/_shared/Icon/Icon.tsx'], session: ['./src/context/session.tsx'], + localize: ['./src/context/localize.tsx'], editor: ['./src/context/editor.tsx'], connect: ['./src/context/connect.tsx'] } From c959a2bba48e93f9334221f677a5e7af4bf13acf Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 09:51:58 +0300 Subject: [PATCH 03/23] fmt --- src/components/Views/EditView/EditView.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index cfb1b4d3..751f8182 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -1,15 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { - Show, - createEffect, - createMemo, - createSignal, - lazy, - on, - onCleanup, - onMount -} from 'solid-js' +import { Show, createEffect, createMemo, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' import { DropArea } from '~/components/_shared/DropArea' @@ -99,7 +90,7 @@ export const EditView = (props: Props) => { const stored = getDraftFromLocalStorage(shout.id) if (stored) { // console.info(`[EditView] got stored shout: ${stored}`) - setDraft((old) => ({...old, ...stored} as Shout)) + setDraft((old) => ({ ...old, ...stored }) as Shout) } else { if (!shout.slug) { console.warn(`[EditView] shout has no slug! ${shout}`) From 728872548023affe1049a29ad0836c217e8836aa Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 10:59:54 +0300 Subject: [PATCH 04/23] new-draft-store-fix --- src/components/Views/EditView/EditView.tsx | 2 +- src/routes/edit/new.tsx | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 751f8182..26d78db7 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -73,7 +73,7 @@ export const EditView = (props: Props) => { const [isLeadVisible, setIsLeadVisible] = createSignal(Boolean(form.lead)) const [isScrolled, setIsScrolled] = createSignal(false) const [shoutTopics, setShoutTopics] = createSignal([]) - const [draft, setDraft] = createSignal() + const [draft, setDraft] = createSignal(props.shout) const [mediaItems, setMediaItems] = createSignal([]) const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) diff --git a/src/routes/edit/new.tsx b/src/routes/edit/new.tsx index 5ae69fdb..7646d0c5 100644 --- a/src/routes/edit/new.tsx +++ b/src/routes/edit/new.tsx @@ -6,6 +6,7 @@ import { Button } from '~/components/_shared/Button' import { Icon } from '~/components/_shared/Icon' import { PageLayout } from '~/components/_shared/PageLayout' import { coreApiUrl } from '~/config' +import { useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' @@ -17,6 +18,7 @@ import { LayoutType } from '~/types/common' export default () => { const { t } = useLocalize() const { session } = useSession() + const { saveDraftToLocalStorage } = useEditorContext() const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const { showSnackbar } = useSnackbar() @@ -29,12 +31,23 @@ export default () => { if (result) { console.debug(result) const { shout, error } = result.data.create_shout - if (error) + if (error) { showSnackbar({ body: `${t('Error')}: ${t(error)}`, type: 'error' }) - if (shout?.id) navigate(`/edit/${shout.id}`) + return + } + if (shout?.id) { + saveDraftToLocalStorage({ + shoutId: shout.id, + selectedTopics: shout.topics, + slug: shout.slug, + title: '', + body: '' + }) + navigate(`/edit/${shout.id}`) + } } } return ( From 29d1661993db38b1330ff1576a04e7293642d285 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 11:03:40 +0300 Subject: [PATCH 05/23] new-shout-desc-fix --- src/routes/edit/new.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/edit/new.tsx b/src/routes/edit/new.tsx index 7646d0c5..326d47ad 100644 --- a/src/routes/edit/new.tsx +++ b/src/routes/edit/new.tsx @@ -54,7 +54,7 @@ export default () => {
From 317d4a004cd1aa984def3b50c62ea649a82e757d Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 12:15:50 +0300 Subject: [PATCH 06/23] graphql-client-unmemo --- src/components/Article/Comment/Comment.tsx | 5 +- src/components/Author/AuthorRatingControl.tsx | 7 +- src/components/Views/Author/Author.tsx | 5 +- .../Views/DraftsView/DraftsView.tsx | 48 ++-- .../Views/EditView/EditSettingsView.tsx | 7 +- src/components/Views/EditView/EditView.tsx | 8 +- src/components/Views/Expo/Expo.tsx | 5 +- src/components/Views/Feed/Feed.tsx | 7 +- src/config.ts | 14 +- src/context/editor.tsx | 7 +- src/context/feed.tsx | 7 +- src/context/following.tsx | 21 +- src/context/inbox.tsx | 7 +- src/context/notifications.tsx | 15 +- src/context/profile.tsx | 18 +- src/context/reactions.tsx | 7 +- src/context/session.tsx | 205 +++++++++++++----- src/intl/locales/ru/translation.json | 1 + src/routes/edit/(drafts).tsx | 8 +- src/routes/edit/[id]/(draft).tsx | 47 ++-- src/routes/edit/[id]/settings.tsx | 36 +-- src/routes/edit/new.tsx | 10 +- src/routes/feed/my/[...mode]/[...order].tsx | 6 +- 23 files changed, 274 insertions(+), 227 deletions(-) diff --git a/src/components/Article/Comment/Comment.tsx b/src/components/Article/Comment/Comment.tsx index 039701d3..6f0125c2 100644 --- a/src/components/Article/Comment/Comment.tsx +++ b/src/components/Article/Comment/Comment.tsx @@ -3,12 +3,10 @@ import { clsx } from 'clsx' import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js' import { Icon } from '~/components/_shared/Icon' import { ShowIfAuthenticated } from '~/components/_shared/ShowIfAuthenticated' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' import { useSnackbar, useUI } from '~/context/ui' -import { graphqlClientCreate } from '~/graphql/client' import deleteReactionMutation from '~/graphql/mutation/core/reaction-destroy' import { Author, @@ -45,12 +43,11 @@ export const Comment = (props: Props) => { const [editMode, setEditMode] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false) const [editedBody, setEditedBody] = createSignal() - const { session } = useSession() + const { session, client } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) const { createShoutReaction, updateShoutReaction } = useReactions() const { showConfirm } = useUI() const { showSnackbar } = useSnackbar() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const canEdit = createMemo( () => Boolean(author()?.id) && diff --git a/src/components/Author/AuthorRatingControl.tsx b/src/components/Author/AuthorRatingControl.tsx index 531ec61e..a002de70 100644 --- a/src/components/Author/AuthorRatingControl.tsx +++ b/src/components/Author/AuthorRatingControl.tsx @@ -1,10 +1,8 @@ import type { Author } from '~/graphql/schema/core.gen' import { clsx } from 'clsx' -import { Show, createMemo, createSignal } from 'solid-js' -import { coreApiUrl } from '~/config' +import { Show, createSignal } from 'solid-js' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import rateAuthorMutation from '~/graphql/mutation/core/author-rate' import styles from './AuthorRatingControl.module.scss' @@ -17,8 +15,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => { const isUpvoted = false const isDownvoted = false - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() // eslint-disable-next-line unicorn/consistent-function-scoping const handleRatingChange = async (isUpvote: boolean) => { diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 1deaa13b..7e1003fc 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -3,7 +3,6 @@ import { clsx } from 'clsx' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { Loading } from '~/components/_shared/Loading' -import { coreApiUrl } from '~/config' import { useAuthors } from '~/context/authors' import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { useFollowing } from '~/context/following' @@ -11,7 +10,6 @@ import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' import { useSession } from '~/context/session' import { loadReactions, loadShouts } from '~/graphql/api/public' -import { graphqlClientCreate } from '~/graphql/client' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' @@ -45,8 +43,7 @@ export const AuthorView = (props: AuthorViewProps) => { const params = useParams() const [currentTab, setCurrentTab] = createSignal(params.tab) - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { session, client } = useSession() const { loadAuthor, authorsEntities } = useAuthors() const { followers: myFollowers, follows: myFollows } = useFollowing() diff --git a/src/components/Views/DraftsView/DraftsView.tsx b/src/components/Views/DraftsView/DraftsView.tsx index cdaae1e1..c2de14a2 100644 --- a/src/components/Views/DraftsView/DraftsView.tsx +++ b/src/components/Views/DraftsView/DraftsView.tsx @@ -1,17 +1,14 @@ import { useNavigate } from '@solidjs/router' import { clsx } from 'clsx' -import { For, Show, createMemo, createSignal } from 'solid-js' +import { For, Show, createSignal } from 'solid-js' import { Draft } from '~/components/Draft' -import { Loading } from '~/components/_shared/Loading' import { useEditorContext } from '~/context/editor' -import { useSession } from '~/context/session' +import { useLocalize } from '~/context/localize' import { Shout } from '~/graphql/schema/core.gen' import styles from './DraftsView.module.scss' export const DraftsView = (props: { drafts: Shout[] }) => { const [drafts, setDrafts] = createSignal(props.drafts || []) - const { session } = useSession() - const authorized = createMemo(() => Boolean(session()?.access_token)) const navigate = useNavigate() const { publishShoutById, deleteShout } = useEditorContext() const handleDraftDelete = async (shout: Shout) => { @@ -26,26 +23,33 @@ export const DraftsView = (props: { drafts: Shout[] }) => { setTimeout(() => navigate('/feed'), 2000) } + const { t } = useLocalize() + return (
- }> -
-
-
- - {(draft) => ( - - )} - -
-
+
+
+

{t('Drafts')}

- + + {(ddd) => ( +
+
+ + {(draft) => ( + + )} + +
+
+ )} +
+
) } diff --git a/src/components/Views/EditView/EditSettingsView.tsx b/src/components/Views/EditView/EditSettingsView.tsx index a410f9a4..351c42c7 100644 --- a/src/components/Views/EditView/EditSettingsView.tsx +++ b/src/components/Views/EditView/EditSettingsView.tsx @@ -1,15 +1,13 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' -import { coreApiUrl } from '~/config' import { ShoutForm, useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { isDesktop } from '~/lib/mediaQuery' @@ -44,8 +42,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditSettingsView = (props: Props) => { const { t } = useLocalize() const [isScrolled, setIsScrolled] = createSignal(false) - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const { form, setForm, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext() const [shoutTopics, setShoutTopics] = createSignal([]) const [draft, setDraft] = createSignal() diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 26d78db7..0e22f777 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -1,6 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' -import { Show, createEffect, createMemo, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' +import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' import { DropArea } from '~/components/_shared/DropArea' @@ -9,11 +9,9 @@ import { InviteMembers } from '~/components/_shared/InviteMembers' import { Loading } from '~/components/_shared/Loading' import { Popover } from '~/components/_shared/Popover' import { EditorSwiper } from '~/components/_shared/SolidSwiper' -import { coreApiUrl } from '~/config' import { ShoutForm, useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { slugify } from '~/intl/translit' @@ -55,7 +53,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditView = (props: Props) => { const { t } = useLocalize() - const { session } = useSession() + const { client } = useSession() const { form, formErrors, @@ -76,8 +74,6 @@ export const EditView = (props: Props) => { const [draft, setDraft] = createSignal(props.shout) const [mediaItems, setMediaItems] = createSignal([]) - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - createEffect(() => setMediaItems(JSON.parse(form.media || '[]'))) createEffect( diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 6ecc017b..294a9748 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -5,12 +5,10 @@ import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { Loading } from '~/components/_shared/Loading' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' -import { coreApiUrl } from '~/config' import { EXPO_LAYOUTS, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { loadShouts } from '~/graphql/api/public' -import { graphqlClientCreate } from '~/graphql/client' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' @@ -31,8 +29,7 @@ const LOAD_MORE_PAGE_SIZE = 12 export const Expo = (props: Props) => { const { t } = useLocalize() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index b083104d..3fc889db 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -7,7 +7,6 @@ import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' import { Loading } from '~/components/_shared/Loading' import { ShareModal } from '~/components/_shared/ShareModal' -import { coreApiUrl } from '~/config' import { useAuthors } from '~/context/authors' import { useLocalize } from '~/context/localize' import { useReactions } from '~/context/reactions' @@ -15,7 +14,6 @@ import { useSession } from '~/context/session' import { useTopics } from '~/context/topics' import { useUI } from '~/context/ui' import { loadUnratedShouts } from '~/graphql/api/private' -import { graphqlClientCreate } from '~/graphql/client' import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' import { FeedSearchParams } from '~/routes/feed/[...order]' import { byCreated } from '~/utils/sort' @@ -49,11 +47,10 @@ const PERIODS = { export const FeedView = (props: FeedProps) => { const { t } = useLocalize() const loc = useLocation() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client, session } = useSession() const unrated = createAsync(async () => { - if (client) { + if (client()) { const shoutsLoader = loadUnratedShouts(client(), { limit: 5 }) return await shoutsLoader() } diff --git a/src/config.ts b/src/config.ts index 7842ada7..f34c0972 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,17 @@ -export const isDev = import.meta.env.MODE === 'development' export const cdnUrl = 'https://cdn.discours.io' export const thumborUrl = import.meta.env.PUBLIC_THUMBOR_URL || 'https://images.discours.io' -export const reportDsn = import.meta.env.PUBLIC_GLITCHTIP_DSN || import.meta.env.PUBLIC_SENTRY_DSN || '' export const coreApiUrl = import.meta.env.PUBLIC_CORE_API || 'https://core.discours.io' export const chatApiUrl = import.meta.env.PUBLIC_CHAT_API || 'https://inbox.discours.io' export const authApiUrl = import.meta.env.PUBLIC_AUTH_API || 'https://auth.discours.io/graphql' export const sseUrl = import.meta.env.PUBLIC_REALTIME_EVENTS || 'https://connect.discours.io' -export const gaIdentity = import.meta.env.PUBLIC_GA_IDENTITY || '' // 'G-LQ4B87H8C2' +export const gaIdentity = import.meta.env.PUBLIC_GA_IDENTITY || 'G-LQ4B87H8C2' +export const authorizerClientId = + import.meta.env.PUBLIC_AUTHORIZER_CLIENT_ID || 'b9038a34-ca59-41ae-a105-c7fbea603e24' +export const authorizerRedirectUrl = + import.meta.env.PUBLIC_AUTHORIZER_REDIRECT_URL || 'https://testing.discours.io' + +// devmode only +export const isDev = import.meta.env.MODE === 'development' +export const reportDsn = isDev + ? import.meta.env.PUBLIC_GLITCHTIP_DSN || import.meta.env.PUBLIC_SENTRY_DSN || '' + : '' diff --git a/src/context/editor.tsx b/src/context/editor.tsx index 83ef43e7..3e3f569b 100644 --- a/src/context/editor.tsx +++ b/src/context/editor.tsx @@ -1,17 +1,15 @@ import { useMatch, useNavigate } from '@solidjs/router' import { Editor, EditorOptions } from '@tiptap/core' import type { JSX } from 'solid-js' -import { Accessor, createContext, createMemo, createSignal, useContext } from 'solid-js' +import { Accessor, createContext, createSignal, useContext } from 'solid-js' import { SetStoreFunction, createStore } from 'solid-js/store' import { createTiptapEditor } from 'solid-tiptap' -import { coreApiUrl } from '~/config' import { useSnackbar } from '~/context/ui' import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import updateShoutQuery from '~/graphql/mutation/core/article-update' import { Topic, TopicInput } from '~/graphql/schema/core.gen' import { slugify } from '~/intl/translit' import { useFeed } from '../context/feed' -import { graphqlClientCreate } from '../graphql/client' import { useLocalize } from './localize' import { useSession } from './session' @@ -85,8 +83,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => { const navigate = useNavigate() const matchEdit = useMatch(() => '/edit') const matchEditSettings = useMatch(() => '/editSettings') - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const [editor, setEditor] = createSignal() const { addFeed } = useFeed() const snackbar = useSnackbar() diff --git a/src/context/feed.tsx b/src/context/feed.tsx index 3380c1ac..7ce2c0c2 100644 --- a/src/context/feed.tsx +++ b/src/context/feed.tsx @@ -1,7 +1,6 @@ import { createLazyMemo } from '@solid-primitives/memo' import { makePersisted } from '@solid-primitives/storage' -import { Accessor, JSX, Setter, createContext, createMemo, createSignal, useContext } from 'solid-js' -import { coreApiUrl } from '~/config' +import { Accessor, JSX, Setter, createContext, createSignal, useContext } from 'solid-js' import { loadFollowedShouts } from '~/graphql/api/private' import { loadShoutsSearch as fetchShoutsSearch, getShout, loadShouts } from '~/graphql/api/public' import { @@ -12,7 +11,6 @@ import { Topic } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' -import { graphqlClientCreate } from '../graphql/client' import { byStat } from '../utils/sort' import { useSession } from './session' @@ -176,8 +174,7 @@ export const FeedProvider = (props: { children: JSX.Element }) => { addFeed(result) return { hasMore, newShouts: result } } - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() // Load the user's feed based on the provided options and update the articleEntities and sortedFeed state const loadMyFeed = async ( diff --git a/src/context/following.tsx b/src/context/following.tsx index 4ae3e25c..98432dd3 100644 --- a/src/context/following.tsx +++ b/src/context/following.tsx @@ -1,21 +1,10 @@ -import { - Accessor, - JSX, - createContext, - createEffect, - createMemo, - createSignal, - on, - useContext -} from 'solid-js' +import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js' import { createStore } from 'solid-js/store' -import { coreApiUrl } from '~/config' import followMutation from '~/graphql/mutation/core/follow' import unfollowMutation from '~/graphql/mutation/core/unfollow' import loadAuthorFollowers from '~/graphql/query/core/author-followers' import { Author, Community, FollowingEntity, Topic } from '~/graphql/schema/core.gen' -import { graphqlClientCreate } from '../graphql/client' import { useSession } from './session' export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities' @@ -70,9 +59,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { const [loading, setLoading] = createSignal(false) const [followers, setFollowers] = createSignal([] as Author[]) const [follows, setFollows] = createStore(EMPTY_SUBSCRIPTIONS) - const { session } = useSession() - const authorized = createMemo(() => Boolean(session()?.access_token)) - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { session, client } = useSession() const fetchData = async () => { setLoading(true) @@ -96,7 +83,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { const [following, setFollowing] = createSignal(defaultFollowing) const follow = async (what: FollowingEntity, slug: string) => { - if (!authorized()) return + if (!session()?.access_token) return setFollowing({ slug, type: 'follow' }) try { const resp = await client()?.mutation(followMutation, { what, slug }).toPromise() @@ -115,7 +102,7 @@ export const FollowingProvider = (props: { children: JSX.Element }) => { } const unfollow = async (what: FollowingEntity, slug: string) => { - if (!authorized()) return + if (!session()?.access_token) return setFollowing({ slug: slug, type: 'unfollow' }) try { const resp = await client()?.mutation(unfollowMutation, { what, slug }).toPromise() diff --git a/src/context/inbox.tsx b/src/context/inbox.tsx index c95b4719..261ae0e6 100644 --- a/src/context/inbox.tsx +++ b/src/context/inbox.tsx @@ -1,7 +1,5 @@ import type { Accessor, JSX } from 'solid-js' -import { createContext, createMemo, createSignal, useContext } from 'solid-js' -import { chatApiUrl } from '~/config' -import { graphqlClientCreate } from '~/graphql/client' +import { createContext, createSignal, useContext } from 'solid-js' import createChatMutation from '~/graphql/mutation/chat/chat-create' import createMessageMutation from '~/graphql/mutation/chat/chat-message-create' import loadChatMessagesQuery from '~/graphql/query/chat/chat-messages-load-by' @@ -38,8 +36,7 @@ export const InboxProvider = (props: { children: JSX.Element }) => { const [chats, setChats] = createSignal([]) const [messages, setMessages] = createSignal([]) const { authorsSorted } = useAuthors() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(chatApiUrl, session()?.access_token)) + const { client } = useSession() const handleMessage = (sseMessage: SSEMessage) => { // handling all action types: create update delete join left seen diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx index 591898f3..ddf6e333 100644 --- a/src/context/notifications.tsx +++ b/src/context/notifications.tsx @@ -1,12 +1,9 @@ import { makePersisted } from '@solid-primitives/storage' import type { Accessor, JSX } from 'solid-js' - import { createContext, createMemo, createSignal, onMount, useContext } from 'solid-js' import { createStore } from 'solid-js/store' import { Portal } from 'solid-js/web' -import { coreApiUrl } from '~/config' -import { graphqlClientCreate } from '~/graphql/client' import markSeenMutation from '~/graphql/mutation/notifier/mark-seen' import markSeenAfterMutation from '~/graphql/mutation/notifier/mark-seen-after' import markSeenThreadMutation from '~/graphql/mutation/notifier/mark-seen-thread' @@ -47,13 +44,11 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0) const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0) const [notificationEntities, setNotificationEntities] = createStore>({}) - const { session } = useSession() - const authorized = createMemo(() => Boolean(session()?.access_token)) + const { session, client } = useSession() const { addHandler } = useConnect() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const loadNotificationsGrouped = async (options: QueryLoad_NotificationsArgs) => { - if (authorized()) { + if (session()?.access_token) { const resp = await client()?.query(getNotifications, options).toPromise() const result = resp?.data?.get_notifications const groups = result?.notifications || [] @@ -87,7 +82,7 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { onMount(() => { addHandler((data: SSEMessage) => { - if (data.entity === 'reaction' && authorized()) { + if (data.entity === 'reaction' && session()?.access_token) { console.info('[context.notifications] event', data) loadNotificationsGrouped({ after: after() || now, @@ -107,14 +102,14 @@ export const NotificationsProvider = (props: { children: JSX.Element }) => { } const markSeenAll = async () => { - if (authorized()) { + if (session()?.access_token) { const _resp = await client()?.mutation(markSeenAfterMutation, { after: after() }).toPromise() await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() }) } } const markSeen = async (notification_id: number) => { - if (authorized()) { + if (session()?.access_token) { await client()?.mutation(markSeenMutation, { notification_id }).toPromise() await loadNotificationsGrouped({ after: after() || now, limit: loadedNotificationsCount() }) } diff --git a/src/context/profile.tsx b/src/context/profile.tsx index 7446732a..0a177029 100644 --- a/src/context/profile.tsx +++ b/src/context/profile.tsx @@ -1,20 +1,9 @@ import type { Author, ProfileInput } from '~/graphql/schema/core.gen' import { AuthToken } from '@authorizerdev/authorizer-js' -import { - Accessor, - JSX, - createContext, - createEffect, - createMemo, - createSignal, - on, - useContext -} from 'solid-js' +import { Accessor, JSX, createContext, createEffect, createSignal, on, useContext } from 'solid-js' import { createStore } from 'solid-js/store' -import { coreApiUrl } from '~/config' import updateAuthorMuatation from '~/graphql/mutation/core/author-update' -import { graphqlClientCreate } from '../graphql/client' import { useAuthors } from './authors' import { useSession } from './session' @@ -41,8 +30,7 @@ const userpicUrl = (userpic: string) => { } export const ProfileProvider = (props: { children: JSX.Element }) => { - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { session, client } = useSession() const { addAuthor } = useAuthors() const [form, setForm] = createStore({} as ProfileInput) const [author, setAuthor] = createSignal({} as Author) @@ -66,7 +54,7 @@ export const ProfileProvider = (props: { children: JSX.Element }) => { const submit = async (profile: ProfileInput) => { const response = await client()?.mutation(updateAuthorMuatation, profile).toPromise() - if (response.error) { + if (response?.error) { console.error(response.error) throw response.error } diff --git a/src/context/reactions.tsx b/src/context/reactions.tsx index 911d7352..583da997 100644 --- a/src/context/reactions.tsx +++ b/src/context/reactions.tsx @@ -1,6 +1,5 @@ import type { Accessor, JSX } from 'solid-js' -import { createContext, createMemo, createSignal, onCleanup, useContext } from 'solid-js' -import { coreApiUrl } from '~/config' +import { createContext, createSignal, onCleanup, useContext } from 'solid-js' import { loadReactions } from '~/graphql/api/public' import createReactionMutation from '~/graphql/mutation/core/reaction-create' import destroyReactionMutation from '~/graphql/mutation/core/reaction-destroy' @@ -12,7 +11,6 @@ import { Reaction, ReactionKind } from '~/graphql/schema/core.gen' -import { graphqlClientCreate } from '../graphql/client' import { useLocalize } from './localize' import { useSession } from './session' import { useSnackbar } from './ui' @@ -41,8 +39,7 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => { const [commentsByAuthor, setCommentsByAuthor] = createSignal>({}) const { t } = useLocalize() const { showSnackbar } = useSnackbar() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() const addShoutReactions = (rrr: Reaction[]) => { const newReactionEntities = { ...reactionEntities() } diff --git a/src/context/session.tsx b/src/context/session.tsx index 12a3f072..a4f58ba3 100644 --- a/src/context/session.tsx +++ b/src/context/session.tsx @@ -12,6 +12,7 @@ import { VerifyEmailInput } from '@authorizerdev/authorizer-js' import { useSearchParams } from '@solidjs/router' +import { Client } from '@urql/core' import type { Accessor, JSX, Resource } from 'solid-js' import { createContext, @@ -25,13 +26,14 @@ import { useContext } from 'solid-js' import { type AuthModalSource, useSnackbar, useUI } from '~/context/ui' -import { authApiUrl } from '../config' +import { graphqlClientCreate } from '~/graphql/client' +import { authApiUrl, authorizerClientId, authorizerRedirectUrl, coreApiUrl } from '../config' import { useLocalize } from './localize' const defaultConfig: ConfigType = { authorizerURL: authApiUrl.replace('/graphql', ''), - redirectURL: 'https://testing.discours.io', - clientID: 'b9038a34-ca59-41ae-a105-c7fbea603e24' + redirectURL: authorizerRedirectUrl, + clientID: authorizerClientId } export type SessionContextType = { @@ -51,20 +53,22 @@ export type SessionContextType = { signOut: () => Promise oauth: (provider: string) => Promise forgotPassword: (params: ForgotPasswordInput) => Promise - changePassword: (password: string, token: string) => void + changePassword: (password: string, token: string) => Promise confirmEmail: (input: VerifyEmailInput) => Promise setIsSessionLoaded: (loaded: boolean) => void authorizer: () => Authorizer isRegistered: (email: string) => Promise resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise + client: Accessor } const noop = () => null + const metaRes = { data: { meta: { version: 'latest', - client_id: 'b9038a34-ca59-41ae-a105-c7fbea603e24', + client_id: authorizerClientId, is_google_login_enabled: true, is_facebook_login_enabled: true, is_github_login_enabled: true, @@ -86,12 +90,21 @@ const metaRes = { } } +/** + * Session context to manage authentication state and provide authentication functions. + */ export const SessionContext = createContext({} as SessionContextType) export function useSession() { return useContext(SessionContext) } +/** + * SessionProvider component that wraps its children with session context. + * It handles session management, authentication, and provides related functions. + * @param props - The props containing an onStateChangeCallback function and children elements. + * @returns A JSX Element wrapping the children with session context. + */ export const SessionProvider = (props: { onStateChangeCallback(state: AuthToken): unknown children: JSX.Element @@ -113,45 +126,55 @@ export const SessionProvider = (props: { const authorizer = createMemo(() => new Authorizer(config())) const [oauthState, setOauthState] = createSignal() - // load - let minuteLater: NodeJS.Timeout | null + // Session expiration timer + let minuteLater: ReturnType | null = null const [isSessionLoaded, setIsSessionLoaded] = createSignal(false) const [authError, setAuthError] = createSignal('') const { showModal } = useUI() - // handle auth state callback from outside + // Handle auth state callback from outside onMount(() => { const params = searchParams if (params?.state) { - setOauthState((_s) => params?.state) - const scope = params?.scope ? params?.scope?.toString().split(' ') : ['openid', 'profile', 'email'] + setOauthState(params.state) + const scope = params.scope ? params.scope.toString().split(' ') : ['openid', 'profile', 'email'] if (scope) console.info(`[context.session] scope: ${scope}`) - const url = params?.redirect_uri || params?.redirectURL || window.location.href + const url = params.redirect_uri || params.redirectURL || window.location.href setConfig((c: ConfigType) => ({ ...c, redirectURL: url.split('?')[0] })) changeSearchParams({ mode: 'confirm-email', m: 'auth' }, { replace: true }) } }) - // handle token confirm + // Handle token confirmation createEffect(() => { const token = searchParams?.token const access_token = searchParams?.access_token - if (access_token) - changeSearchParams({ - mode: 'confirm-email', - m: 'auth', - access_token - }) - else if (token) { - changeSearchParams({ - mode: 'change-password', - m: 'auth', - token - }) + if (access_token) { + changeSearchParams( + { + mode: 'confirm-email', + m: 'auth', + access_token + }, + { replace: true } + ) + } else if (token) { + changeSearchParams( + { + mode: 'change-password', + m: 'auth', + token + }, + { replace: true } + ) } }) - // Function to load session data + /** + * Function to load session data by fetching the current session from the authorizer. + * It handles session expiration and sets up a timer to refresh the session as needed. + * @returns A Promise resolving to the AuthToken containing session information. + */ const sessionData = async () => { try { const s: ApiResponse = await authorizer().getSession() @@ -191,6 +214,10 @@ export const SessionProvider = (props: { initialValue: {} as AuthToken }) + /** + * Checks if the current session has expired and refreshes the session if necessary. + * Sets up a timer to check the session expiration every minute. + */ const checkSessionIsExpired = () => { const expires_at_data = localStorage?.getItem('expires_at') @@ -209,9 +236,11 @@ export const SessionProvider = (props: { } } - onCleanup(() => clearTimeout(minuteLater as NodeJS.Timeout)) + onCleanup(() => { + if (minuteLater) clearTimeout(minuteLater) + }) - // initial effect + // Initial effect onMount(() => { setConfig({ ...defaultConfig, @@ -221,16 +250,23 @@ export const SessionProvider = (props: { loadSession() }) - // callback state updater + // Callback state updater createEffect( on([() => props.onStateChangeCallback, session], ([_, ses]) => { - ses?.user?.id && props.onStateChangeCallback(ses) + if (ses?.user?.id) props.onStateChangeCallback(ses) }) ) const [authCallback, setAuthCallback] = createSignal<() => void>(noop) + + /** + * Requires the user to be authenticated before executing a callback function. + * If the user is not authenticated, it shows the authentication modal. + * @param callback - The function to execute after authentication. + * @param modalSource - The source of the authentication modal. + */ const requireAuthentication = (callback: () => void, modalSource: AuthModalSource) => { - setAuthCallback((_cb) => callback) + setAuthCallback(() => callback) if (!session()) { loadSession() if (!session()) { @@ -243,23 +279,36 @@ export const SessionProvider = (props: { const handler = authCallback() if (handler !== noop) { handler() - setAuthCallback((_cb) => noop) + setAuthCallback(() => noop) } }) - // authorizer api proxy methods + /** + * General function to authenticate a user using a specified authentication function. + * @param authFunction - The authentication function to use (e.g., signup, login). + * @param params - The parameters to pass to the authentication function. + * @returns An object containing data and errors from the authentication attempt. + */ + type AuthFunctionType = ( + data: SignupInput | LoginInput | UpdateProfileInput + ) => Promise> const authenticate = async ( - authFunction: (data: SignupInput) => Promise>, - // biome-ignore lint/suspicious/noExplicitAny: authorizer - params: any + authFunction: AuthFunctionType, + params: SignupInput | LoginInput | UpdateProfileInput ) => { const resp = await authFunction(params) console.debug('[context.session] authenticate:', resp) if (resp?.data && resp?.errors.length === 0) setSession(resp.data as AuthToken) return { data: resp?.data, errors: resp?.errors } } + + /** + * Signs up a new user using the provided parameters. + * @param params - The signup input parameters. + * @returns A Promise resolving to `true` if signup was successful, otherwise `false`. + */ const signUp = async (params: SignupInput): Promise => { - const resp = await authenticate(authorizer().signup, params as SignupInput) + const resp = await authenticate(authorizer().signup as AuthFunctionType, params as SignupInput) console.debug('[context.session] signUp:', resp) if (resp?.data) { setSession(resp.data as AuthToken) @@ -268,8 +317,13 @@ export const SessionProvider = (props: { return false } + /** + * Signs in a user using the provided credentials. + * @param params - The login input parameters. + * @returns A Promise resolving to `true` if sign-in was successful, otherwise `false`. + */ const signIn = async (params: LoginInput): Promise => { - const resp = await authenticate(authorizer().login, params as LoginInput) + const resp = await authenticate(authorizer().login as AuthFunctionType, params) console.debug('[context.session] signIn:', resp) if (resp?.data) { setSession(resp.data as AuthToken) @@ -280,61 +334,97 @@ export const SessionProvider = (props: { return false } - const updateProfile = async (params: UpdateProfileInput) => { - const resp = await authenticate(authorizer().updateProfile, params as UpdateProfileInput) + /** + * Updates the user's profile with the provided parameters. + * @param params - The update profile input parameters. + * @returns A Promise resolving to `true` if the update was successful, otherwise `false`. + */ + const updateProfile = async (params: UpdateProfileInput): Promise => { + const resp = await authenticate(authorizer().updateProfile, params) console.debug('[context.session] updateProfile response:', resp) if (resp?.data) { - // console.debug('[context.session] response data ', resp.data) - // FIXME: renew updated profile + // Optionally refresh session or user data here return true } return false } - const signOut = async () => { + /** + * Signs out the current user and clears the session. + * @returns A Promise resolving to `true` if sign-out was successful. + */ + const signOut = async (): Promise => { const authResult: ApiResponse = await authorizer().logout() - // console.debug('[context.session] sign out', authResult) if (authResult) { setSession({} as AuthToken) setIsSessionLoaded(true) showSnackbar({ body: t("You've successfully logged out") }) - // console.debug(session()) return true } return false } - const changePassword = async (password: string, token: string) => { + /** + * Changes the user's password using a token from a password reset email. + * @param password - The new password. + * @param token - The token from the password reset email. + * @returns A Promise resolving to `true` if the password was changed successfully. + */ + const changePassword = async (password: string, token: string): Promise => { const resp = await authorizer().resetPassword({ password, token, confirm_password: password }) console.debug('[context.session] change password response:', resp) + if (resp.data) { + return true + } + return false } - const forgotPassword = async (params: ForgotPasswordInput) => { + /** + * Initiates the forgot password process for the given email. + * @param params - The forgot password input parameters. + * @returns A Promise resolving to an error message if any, otherwise an empty string. + */ + const forgotPassword = async (params: ForgotPasswordInput): Promise => { const resp = await authorizer().forgotPassword(params) - console.debug('[context.session] change password response:', resp) - return resp?.errors?.pop()?.message || '' + console.debug('[context.session] forgot password response:', resp) + if (resp.errors.length > 0) { + return resp.errors.pop()?.message || '' + } + return '' } + /** + * Resends the verification email to the user. + * @param params - The resend verify email input parameters. + * @returns A Promise resolving to `true` if the email was sent successfully. + */ const resendVerifyEmail = async (params: ResendVerifyEmailInput): Promise => { - const resp = await authorizer().resendVerifyEmail(params as ResendVerifyEmailInput) + const resp = await authorizer().resendVerifyEmail(params) console.debug('[context.session] resend verify email response:', resp) - if (resp.errors) { + if (resp.errors.length > 0) { resp.errors.forEach((error) => { showSnackbar({ type: 'error', body: error.message }) }) + return false } - return resp ? resp.data?.message === 'Verification email has been sent. Please check your inbox' : false + return resp.data?.message === 'Verification email has been sent. Please check your inbox' } + /** + * Checks if an email is already registered. + * @param email - The email to check. + * @returns A Promise resolving to the message from the server indicating the registration status. + */ const isRegistered = async (email: string): Promise => { console.debug('[context.session] calling is_registered for ', email) try { const response = await authorizer().graphqlQuery({ - query: `query { is_registered(email: "${email}") { message }}` + query: 'query IsRegistered($email: String!) { is_registered(email: $email) { message }}', + variables: { email } }) return response?.data?.is_registered?.message } catch (error) { @@ -361,6 +451,16 @@ export const SessionProvider = (props: { console.warn(error) } } + + // authorized graphql client + const [client, setClient] = createSignal() + createEffect( + on(session, (s?: AuthToken) => { + const tkn = s?.access_token + setClient((_c?: Client) => graphqlClientCreate(coreApiUrl, tkn)) + }) + ) + const actions = { loadSession, requireAuthentication, @@ -378,6 +478,7 @@ export const SessionProvider = (props: { isRegistered } const value: SessionContextType = { + client, authError, config, session, diff --git a/src/intl/locales/ru/translation.json b/src/intl/locales/ru/translation.json index 4e0119a1..4169bc12 100644 --- a/src/intl/locales/ru/translation.json +++ b/src/intl/locales/ru/translation.json @@ -295,6 +295,7 @@ "New stories and more are waiting for you every day!": "Каждый день вас ждут новые истории и ещё много всего интересного!", "Newsletter": "Рассылка", "Night mode": "Ночная тема", + "No drafts": "Нет черновиков", "No notifications yet": "Уведомлений пока нет", "No such account, please try to register": "Такой адрес не найден, попробуйте зарегистрироваться", "not verified": "ещё не подтверждён", diff --git a/src/routes/edit/(drafts).tsx b/src/routes/edit/(drafts).tsx index 3f8b4ca9..919f04f0 100644 --- a/src/routes/edit/(drafts).tsx +++ b/src/routes/edit/(drafts).tsx @@ -1,13 +1,10 @@ import { createAsync } from '@solidjs/router' import { Client } from '@urql/core' -import { createMemo } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import { DraftsView } from '~/components/Views/DraftsView' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getDraftsQuery from '~/graphql/query/core/articles-load-drafts' import { Shout } from '~/graphql/schema/core.gen' @@ -19,9 +16,8 @@ const fetchDrafts = async (client: Client) => { export default () => { const { t } = useLocalize() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - const drafts = createAsync(async () => await fetchDrafts(client())) + const { client } = useSession() + const drafts = createAsync(async () => client() && (await fetchDrafts(client() as Client))) return ( diff --git a/src/routes/edit/[id]/(draft).tsx b/src/routes/edit/[id]/(draft).tsx index d494ce48..52bdd6ca 100644 --- a/src/routes/edit/[id]/(draft).tsx +++ b/src/routes/edit/[id]/(draft).tsx @@ -2,11 +2,9 @@ import { RouteSectionProps, redirect } from '@solidjs/router' import { createEffect, createMemo, createSignal, lazy, on } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' -import { graphqlClientCreate } from '~/graphql/client' import getShoutDraft from '~/graphql/query/core/article-my' import { Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' @@ -15,31 +13,32 @@ const EditView = lazy(() => import('~/components/Views/EditView/EditView')) export default (props: RouteSectionProps) => { const { t } = useLocalize() - const { session } = useSession() + const { session, client } = useSession() const snackbar = useSnackbar() - const fail = async (error: string) => { - console.error(error) - const errorMessage = error === 'forbidden' ? "You can't edit this post" : error - await snackbar?.showSnackbar({ type: 'error', body: t(errorMessage) }) - redirect('/edit') // all drafts page - } const [shout, setShout] = createSignal() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - createEffect(on(session, (s) => s?.access_token && loadDraft(), { defer: true })) - - const loadDraft = async () => { - const shout_id = Number.parseInt(props.params.id) - const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() - if (result) { - const { shout: loadedShout, error } = result.data.get_my_shout - if (error) { - fail(error) - } else { - setShout(loadedShout) - } - } - } + createEffect( + on( + session, + async (s) => { + if (!s?.access_token) return + const shout_id = Number.parseInt(props.params.id) + const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() + if (result) { + const { shout: loadedShout, error } = result.data.get_my_shout + if (error) { + console.error(error) + const errorMessage = error === 'forbidden' ? "You can't edit this post" : error + await snackbar?.showSnackbar({ type: 'error', body: t(errorMessage) }) + redirect('/edit') // all drafts page + } else { + setShout(loadedShout) + } + } + }, + {} + ) + ) const title = createMemo(() => { const layout = (shout()?.layout as LayoutType) || 'article' diff --git a/src/routes/edit/[id]/settings.tsx b/src/routes/edit/[id]/settings.tsx index 7610d02a..9f33d198 100644 --- a/src/routes/edit/[id]/settings.tsx +++ b/src/routes/edit/[id]/settings.tsx @@ -1,30 +1,36 @@ +import { AuthToken } from '@authorizerdev/authorizer-js' import { RouteSectionProps } from '@solidjs/router' -import { createEffect, createMemo, createSignal, on } from 'solid-js' +import { createEffect, createSignal, on } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import EditSettingsView from '~/components/Views/EditView/EditSettingsView' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { graphqlClientCreate } from '~/graphql/client' import getShoutDraft from '~/graphql/query/core/article-my' import { Shout } from '~/graphql/schema/core.gen' export default (props: RouteSectionProps) => { const { t } = useLocalize() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - createEffect(on(session, (s) => s?.access_token && loadDraft(), { defer: true })) + const { session, client } = useSession() const [shout, setShout] = createSignal() - const loadDraft = async () => { - const shout_id = Number.parseInt(props.params.id) - const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() - if (result) { - const { shout: loadedShout, error } = result.data.get_my_shout - if (error) throw new Error(error) - setShout(loadedShout) - } - } + + createEffect( + on( + session, + async (s?: AuthToken) => { + if (!s?.access_token) return + const shout_id = Number.parseInt(props.params.id) + const result = await client()?.query(getShoutDraft, { shout_id }).toPromise() + if (result) { + const { shout: loadedShout, error } = result.data.get_my_shout + if (error) throw new Error(error) + setShout(loadedShout) + } + }, + {} + ) + ) + return ( diff --git a/src/routes/edit/new.tsx b/src/routes/edit/new.tsx index 326d47ad..28d24947 100644 --- a/src/routes/edit/new.tsx +++ b/src/routes/edit/new.tsx @@ -1,25 +1,23 @@ import { useNavigate } from '@solidjs/router' import { clsx } from 'clsx' -import { For, createMemo } from 'solid-js' +import { For } from 'solid-js' import { AuthGuard } from '~/components/AuthGuard' import { Button } from '~/components/_shared/Button' import { Icon } from '~/components/_shared/Icon' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' -import { graphqlClientCreate } from '~/graphql/client' import createShoutMutation from '~/graphql/mutation/core/article-create' -import styles from '~/styles/Create.module.scss' import { LayoutType } from '~/types/common' +import styles from '~/styles/Create.module.scss' + export default () => { const { t } = useLocalize() - const { session } = useSession() + const { client } = useSession() const { saveDraftToLocalStorage } = useEditorContext() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) const { showSnackbar } = useSnackbar() const navigate = useNavigate() diff --git a/src/routes/feed/my/[...mode]/[...order].tsx b/src/routes/feed/my/[...mode]/[...order].tsx index bc7ea929..4155cbf1 100644 --- a/src/routes/feed/my/[...mode]/[...order].tsx +++ b/src/routes/feed/my/[...mode]/[...order].tsx @@ -1,11 +1,11 @@ import { RouteSectionProps, useSearchParams } from '@solidjs/router' import { createEffect, createMemo } from 'solid-js' + import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors' import { Feed } from '~/components/Views/Feed' import { FeedProps } from '~/components/Views/Feed/Feed' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { PageLayout } from '~/components/_shared/PageLayout' -import { coreApiUrl } from '~/config' import { useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' import { ReactionsProvider } from '~/context/reactions' @@ -17,7 +17,6 @@ import { loadFollowedShouts, loadUnratedShouts } from '~/graphql/api/private' -import { graphqlClientCreate } from '~/graphql/client' import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { FromPeriod, getFromDate } from '~/lib/fromPeriod' @@ -38,8 +37,7 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) const [searchParams] = useSearchParams() // ?period=month const { t } = useLocalize() const { setFeed, feed } = useFeed() - const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + const { client } = useSession() // preload all topics const { addTopics, sortedTopics } = useTopics() From 6e3871cd5a1a64e3ed2bbeb054dc4731a981f9eb Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 12:38:53 +0300 Subject: [PATCH 07/23] link-fix1 --- .../TopicSelect/TopicSelect.module.scss | 40 +++++++++++++++++++ src/components/Views/Feed/Feed.tsx | 2 +- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/components/Editor/TopicSelect/TopicSelect.module.scss b/src/components/Editor/TopicSelect/TopicSelect.module.scss index cbbdc062..1639d5c9 100644 --- a/src/components/Editor/TopicSelect/TopicSelect.module.scss +++ b/src/components/Editor/TopicSelect/TopicSelect.module.scss @@ -31,3 +31,43 @@ .TopicSelect .solid-select-option[data-disabled='true'] { display: none; } + +.selectedTopics { + display: flex; + flex-wrap: wrap; +} + +.selectedTopic { + background-color: #f0f0f0; + margin: 4px; + padding: 6px; + border-radius: 4px; +} + +.selectWrapper { + display: inline-block; + cursor: pointer; +} + +.searchInput { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.options { + margin-top: 10px; + background-color: white; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.option { + padding: 10px; + cursor: pointer; +} + +.disabled { + color: gray; + pointer-events: none; +} diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index 3fc889db..3c05557d 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -215,7 +215,7 @@ export const FeedView = (props: FeedProps) => {
From 5e2b4a7ae64ff17e3507d839e76a83a0ae233833 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 13:09:24 +0300 Subject: [PATCH 08/23] canedit-fix --- src/components/Article/FullArticle.tsx | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 76cca037..8383468c 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -1,3 +1,4 @@ +import { AuthToken } from '@authorizerdev/authorizer-js' import { createPopper } from '@popperjs/core' import { Link } from '@solidjs/meta' import { A, useSearchParams } from '@solidjs/router' @@ -73,7 +74,6 @@ export const FullArticle = (props: Props) => { const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const { t, formatDate, lang } = useLocalize() const { session, requireAuthentication } = useSession() - const author = createMemo(() => session()?.user?.app_data?.profile as Author) const { addSeen } = useFeed() const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000))) @@ -100,13 +100,18 @@ export const FullArticle = (props: Props) => { ) ) - const canEdit = createMemo( - () => - Boolean(author()?.id) && - (props.article.authors?.some((a) => Boolean(a) && a?.id === author().id) || - props.article.created_by?.id === author().id || - session()?.user?.roles?.includes('editor')) - ) + const [canEdit, setCanEdit] = createSignal(false) + createEffect(on( + () => session(), + (s?: AuthToken) => { + const profile = s?.user?.app_data?.profile + if (!profile) return + const isEditor = s?.user?.roles?.includes('editor') + const isCreator = props.article.created_by?.id === profile.id + const fit = (a: Maybe) => a?.id === profile.id || isCreator || isEditor + setCanEdit((_: boolean) => Boolean(props.article.authors?.some(fit))) + } + )) const mainTopic = createMemo(() => { const mainTopicSlug = (props.article.topics?.length || 0) > 0 ? props.article.main_topic : null @@ -534,7 +539,7 @@ export const FullArticle = (props: Props) => { />
- +
From 2fef053029c72475bffc9eed0b6d177bd134bae0 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 13:09:24 +0300 Subject: [PATCH 09/23] canedit-fix --- src/components/Article/FullArticle.tsx | 23 ++++++++++++------- .../TopicSelect/TopicSelect.module.scss | 2 +- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 76cca037..7420f8bd 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -1,3 +1,4 @@ +import { AuthToken } from '@authorizerdev/authorizer-js' import { createPopper } from '@popperjs/core' import { Link } from '@solidjs/meta' import { A, useSearchParams } from '@solidjs/router' @@ -73,7 +74,6 @@ export const FullArticle = (props: Props) => { const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const { t, formatDate, lang } = useLocalize() const { session, requireAuthentication } = useSession() - const author = createMemo(() => session()?.user?.app_data?.profile as Author) const { addSeen } = useFeed() const formattedDate = createMemo(() => formatDate(new Date((props.article.published_at || 0) * 1000))) @@ -100,12 +100,19 @@ export const FullArticle = (props: Props) => { ) ) - const canEdit = createMemo( - () => - Boolean(author()?.id) && - (props.article.authors?.some((a) => Boolean(a) && a?.id === author().id) || - props.article.created_by?.id === author().id || - session()?.user?.roles?.includes('editor')) + const [canEdit, setCanEdit] = createSignal(false) + createEffect( + on( + () => session(), + (s?: AuthToken) => { + const profile = s?.user?.app_data?.profile + if (!profile) return + const isEditor = s?.user?.roles?.includes('editor') + const isCreator = props.article.created_by?.id === profile.id + const fit = (a: Maybe) => a?.id === profile.id || isCreator || isEditor + setCanEdit((_: boolean) => Boolean(props.article.authors?.some(fit))) + } + ) ) const mainTopic = createMemo(() => { @@ -534,7 +541,7 @@ export const FullArticle = (props: Props) => { />
- +
diff --git a/src/components/Editor/TopicSelect/TopicSelect.module.scss b/src/components/Editor/TopicSelect/TopicSelect.module.scss index 1639d5c9..1d8c1a19 100644 --- a/src/components/Editor/TopicSelect/TopicSelect.module.scss +++ b/src/components/Editor/TopicSelect/TopicSelect.module.scss @@ -59,7 +59,7 @@ .options { margin-top: 10px; background-color: white; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 8px rgb(0 0 0 / 10%); } .option { From 22575cc7fa179ee170ab3415534d746839683b41 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 24 Sep 2024 14:56:16 +0300 Subject: [PATCH 10/23] expo-showup --- src/components/Views/Expo/Expo.tsx | 63 ++++++++++++++++-------------- src/routes/expo/[...layout].tsx | 22 +++++++---- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index 294a9748..fefd6924 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -1,6 +1,7 @@ import { A } from '@solidjs/router' import { clsx } from 'clsx' import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' + import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { Loading } from '~/components/_shared/Loading' @@ -13,6 +14,8 @@ import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-t import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LayoutType } from '~/types/common' import { getUnixtime } from '~/utils/date' +import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' +import { byCreated } from '~/utils/sort' import { ArticleCard } from '../../Feed/ArticleCard' import styles from './Expo.module.scss' @@ -33,21 +36,9 @@ export const Expo = (props: Props) => { const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) - const [expoShouts, setExpoShouts] = createSignal([]) const { feedByLayout, expoFeed, setExpoFeed } = useFeed() const layouts = createMemo(() => (props.layout ? [props.layout] : EXPO_LAYOUTS)) - const loadMoreFiltered = async () => { - const limit = SHOUTS_PER_PAGE - const offset = (props.layout ? feedByLayout()[props.layout] : expoFeed())?.length - const filters: LoadShoutsFilters = { layouts: layouts(), featured: true } - const options: LoadShoutsOptions = { filters, limit, offset } - const shoutsFetcher = loadShouts(options) - const result = await shoutsFetcher() - result && setExpoFeed(result) - return result as LoadMoreItems - } - const loadRandomTopArticles = async () => { const options: LoadShoutsOptions = { filters: { layouts: layouts(), featured: true }, @@ -76,20 +67,15 @@ export const Expo = (props: Props) => { }) createEffect( - on( - () => props.layout, - () => { - setExpoShouts([]) - setFavoriteTopArticles([]) - setReactedTopMonthArticles([]) - loadRandomTopArticles() - loadRandomTopMonthArticles() - } - ) + on(layouts, (lll) => { + console.debug('layouts changed', lll) + loadRandomTopArticles() + loadRandomTopMonthArticles() + }) ) onCleanup(() => { - setExpoShouts([]) + setExpoFeed([]) }) const ExpoTabs = () => (
@@ -134,10 +120,10 @@ export const Expo = (props: Props) => {
) - const ExpoGrid = () => ( + const ExpoGrid = (props: Props) => (
- + {(shout) => (
{ 0} keyed={true}> - + {(shout) => (
{
) + const [loadMoreVisible, setLoadMoreVisible] = createSignal(false) + + // дозагрузка + const loadMore = async () => { + saveScrollPosition() + const limit = SHOUTS_PER_PAGE + const offset = (props.layout ? feedByLayout()[props.layout] : expoFeed())?.length + const filters: LoadShoutsFilters = { layouts: layouts(), featured: true } + const options: LoadShoutsOptions = { filters, limit, offset } + const shoutsFetcher = loadShouts(options) + const result = await shoutsFetcher() + setLoadMoreVisible(Boolean(result?.length)) + const expoFeedUpdater = (layout?: LayoutType) => (prev: Shout[]) => + Array.from(new Set((layout ? prev || [] : expoFeed())?.concat(result || [])))?.sort(byCreated) + result && setExpoFeed(expoFeedUpdater(props.layout)) + restoreScrollPosition() + return result as LoadMoreItems + } + return (
- 0} fallback={}> - - + }> +
diff --git a/src/routes/expo/[...layout].tsx b/src/routes/expo/[...layout].tsx index bc60e0cf..764fc186 100644 --- a/src/routes/expo/[...layout].tsx +++ b/src/routes/expo/[...layout].tsx @@ -1,5 +1,5 @@ import { Params, RouteSectionProps, createAsync } from '@solidjs/router' -import { Show, createEffect, createMemo, on } from 'solid-js' +import { Show, onMount } from 'solid-js' import { TopicsNav } from '~/components/TopicsNav' import { Expo } from '~/components/Views/Expo' import { PageLayout } from '~/components/_shared/PageLayout' @@ -32,9 +32,9 @@ export default (props: RouteSectionProps) => { async () => props.data || (await fetchExpoShouts(props.params.layout ? [props.params.layout] : EXPO_LAYOUTS)) ) - const layout = createMemo(() => props.params.layout) - const title = createMemo(() => { - switch (layout()) { + + const getTitle = (l: string) => { + switch (l) { case 'audio': { return t('Audio') } @@ -51,15 +51,21 @@ export default (props: RouteSectionProps) => { return t('Art') } } + } + + onMount(() => { + document.title = getTitle(props.params.layout || '') }) - createEffect(on(title, (ttl) => (document.title = ttl), { defer: true })) - return ( - + - {(sss) => } + {(sss) => } ) From a144d7051b3ec9ee4d2662741b830e02011877a5 Mon Sep 17 00:00:00 2001 From: Untone Date: Thu, 26 Sep 2024 01:48:30 +0300 Subject: [PATCH 11/23] expo-fixes --- src/components/Views/Expo/Expo.module.scss | 8 - src/components/Views/Expo/Expo.tsx | 196 +++++---------------- src/components/Views/Expo/ExpoNav.tsx | 34 ++++ src/components/_shared/LoadMoreWrapper.tsx | 24 +-- src/context/feed.tsx | 11 +- src/routes/expo/[...layout].tsx | 80 ++++++--- src/types/common.d.ts | 3 +- 7 files changed, 155 insertions(+), 201 deletions(-) create mode 100644 src/components/Views/Expo/ExpoNav.tsx diff --git a/src/components/Views/Expo/Expo.module.scss b/src/components/Views/Expo/Expo.module.scss index 190b4243..03b02dbe 100644 --- a/src/components/Views/Expo/Expo.module.scss +++ b/src/components/Views/Expo/Expo.module.scss @@ -3,12 +3,4 @@ background: #fef2f2; padding: 0 0 4rem; min-height: 100vh; - - .showMore { - display: flex; - width: 100%; - padding: 4rem 0 2rem; - align-items: center; - justify-content: center; - } } diff --git a/src/components/Views/Expo/Expo.tsx b/src/components/Views/Expo/Expo.tsx index fefd6924..cf9e4066 100644 --- a/src/components/Views/Expo/Expo.tsx +++ b/src/components/Views/Expo/Expo.tsx @@ -1,47 +1,34 @@ -import { A } from '@solidjs/router' -import { clsx } from 'clsx' -import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js' +import { For, Show, createEffect, createSignal, on } from 'solid-js' -import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' -import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { Loading } from '~/components/_shared/Loading' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' -import { EXPO_LAYOUTS, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' +import { EXPO_LAYOUTS, SHOUTS_PER_PAGE } from '~/context/feed' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { loadShouts } from '~/graphql/api/public' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' -import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' -import { LayoutType } from '~/types/common' +import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' +import { ExpoLayoutType } from '~/types/common' import { getUnixtime } from '~/utils/date' -import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' -import { byCreated } from '~/utils/sort' import { ArticleCard } from '../../Feed/ArticleCard' import styles from './Expo.module.scss' type Props = { shouts: Shout[] - topMonthShouts?: Shout[] - topRatedShouts?: Shout[] - layout?: LayoutType + layout: ExpoLayoutType } -export const PRERENDERED_ARTICLES_COUNT = 36 -const LOAD_MORE_PAGE_SIZE = 12 - export const Expo = (props: Props) => { const { t } = useLocalize() const { client } = useSession() - const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) - const { feedByLayout, expoFeed, setExpoFeed } = useFeed() - const layouts = createMemo(() => (props.layout ? [props.layout] : EXPO_LAYOUTS)) + // Функция загрузки случайных избранных статей const loadRandomTopArticles = async () => { + const layouts = props.layout ? [props.layout] : EXPO_LAYOUTS const options: LoadShoutsOptions = { - filters: { layouts: layouts(), featured: true }, + filters: { layouts, featured: true }, limit: 10, random_limit: 100 } @@ -49,11 +36,13 @@ export const Expo = (props: Props) => { setFavoriteTopArticles(resp?.data?.load_shouts_random_top || []) } + // Функция загрузки популярных статей за последний месяц const loadRandomTopMonthArticles = async () => { + const layouts = props.layout ? [props.layout] : EXPO_LAYOUTS const now = new Date() const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1))) const options: LoadShoutsOptions = { - filters: { layouts: layouts(), after, reacted: true }, + filters: { layouts, after, reacted: true }, limit: 10, random_limit: 10 } @@ -61,141 +50,46 @@ export const Expo = (props: Props) => { setReactedTopMonthArticles(resp?.data?.load_shouts_random_top || []) } - onMount(() => { - loadRandomTopArticles() - loadRandomTopMonthArticles() - }) - + // Эффект для загрузки random top при изменении layout createEffect( - on(layouts, (lll) => { - console.debug('layouts changed', lll) - loadRandomTopArticles() - loadRandomTopMonthArticles() - }) + on( + () => props.layout, + async (_layout?: ExpoLayoutType) => { + await loadRandomTopArticles() + await loadRandomTopMonthArticles() + } + ) ) - onCleanup(() => { - setExpoFeed([]) - }) - const ExpoTabs = () => ( -
- -
- ) - const ExpoGrid = (props: Props) => ( -
-
- - {(shout) => ( -
- -
- )} -
- 0} keyed={true}> - - - - {(shout) => ( -
- -
- )} -
- 0} keyed={true}> - - - - {(shout) => ( -
- -
- )} -
-
-
- ) - - const [loadMoreVisible, setLoadMoreVisible] = createSignal(false) - - // дозагрузка - const loadMore = async () => { - saveScrollPosition() - const limit = SHOUTS_PER_PAGE - const offset = (props.layout ? feedByLayout()[props.layout] : expoFeed())?.length - const filters: LoadShoutsFilters = { layouts: layouts(), featured: true } - const options: LoadShoutsOptions = { filters, limit, offset } - const shoutsFetcher = loadShouts(options) - const result = await shoutsFetcher() - setLoadMoreVisible(Boolean(result?.length)) - const expoFeedUpdater = (layout?: LayoutType) => (prev: Shout[]) => - Array.from(new Set((layout ? prev || [] : expoFeed())?.concat(result || [])))?.sort(byCreated) - result && setExpoFeed(expoFeedUpdater(props.layout)) - restoreScrollPosition() - return result as LoadMoreItems - } - return (
- + } keyed> + {(feed: Shout[]) => ( +
+
+ + {(shout) => ( +
+ +
+ )} +
+
- }> - + 0}> + + + + 0}> + + +
+ )}
) diff --git a/src/components/Views/Expo/ExpoNav.tsx b/src/components/Views/Expo/ExpoNav.tsx new file mode 100644 index 00000000..2bc1933d --- /dev/null +++ b/src/components/Views/Expo/ExpoNav.tsx @@ -0,0 +1,34 @@ +import { A } from '@solidjs/router' +import { clsx } from 'clsx' +import { For } from 'solid-js' + +import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' +import { EXPO_LAYOUTS, EXPO_TITLES } from '~/context/feed' +import { useLocalize } from '~/context/localize' +import { ExpoLayoutType } from '~/types/common' + +export const ExpoNav = (props: { layout: ExpoLayoutType | '' }) => { + const { t } = useLocalize() + return ( +
+
    + + {(layoutKey) => ( +
  • + {children}} + > + + {layoutKey in EXPO_TITLES ? t(EXPO_TITLES[layoutKey as ExpoLayoutType]) : t('All')} + + +
  • + )} +
    +
+
+ ) +} + +export default ExpoNav diff --git a/src/components/_shared/LoadMoreWrapper.tsx b/src/components/_shared/LoadMoreWrapper.tsx index ff53e9bd..493e94ef 100644 --- a/src/components/_shared/LoadMoreWrapper.tsx +++ b/src/components/_shared/LoadMoreWrapper.tsx @@ -33,6 +33,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => { ) const loadItems = async () => { + // console.debug('LoadMoreWrapper.loadItems offset:', offset()) setIsLoading(true) saveScrollPosition() const newItems = await props.loadFunction(offset()) @@ -47,6 +48,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => { ) setIsLoading(false) restoreScrollPosition() + // console.debug('LoadMoreWrapper.loadItems loaded:', newItems.length) } onMount(loadItems) @@ -54,16 +56,18 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => { return ( <> {props.children} - -
-
-
+
+ +
+
+
+
) } diff --git a/src/context/feed.tsx b/src/context/feed.tsx index 7ce2c0c2..b6d22375 100644 --- a/src/context/feed.tsx +++ b/src/context/feed.tsx @@ -10,13 +10,20 @@ import { Shout, Topic } from '~/graphql/schema/core.gen' -import { LayoutType } from '~/types/common' +import { ExpoLayoutType } from '~/types/common' import { byStat } from '../utils/sort' import { useSession } from './session' export const PRERENDERED_ARTICLES_COUNT = 5 export const SHOUTS_PER_PAGE = 20 -export const EXPO_LAYOUTS = ['audio', 'literature', 'video', 'image'] as LayoutType[] +export const EXPO_LAYOUTS = ['audio', 'literature', 'video', 'image'] as ExpoLayoutType[] +export const EXPO_TITLES: Record = { + 'audio': 'Audio', + 'video': 'Video', + 'image': 'Artworks', + 'literature': 'Literature', + '': 'All' +} type FeedContextType = { sortedFeed: Accessor diff --git a/src/routes/expo/[...layout].tsx b/src/routes/expo/[...layout].tsx index 764fc186..f0410caf 100644 --- a/src/routes/expo/[...layout].tsx +++ b/src/routes/expo/[...layout].tsx @@ -1,13 +1,17 @@ import { Params, RouteSectionProps, createAsync } from '@solidjs/router' -import { Show, onMount } from 'solid-js' +import { Show, createEffect, createSignal, on } from 'solid-js' import { TopicsNav } from '~/components/TopicsNav' import { Expo } from '~/components/Views/Expo' +import ExpoNav from '~/components/Views/Expo/ExpoNav' +import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { PageLayout } from '~/components/_shared/PageLayout' -import { EXPO_LAYOUTS, SHOUTS_PER_PAGE } from '~/context/feed' +import { EXPO_LAYOUTS, EXPO_TITLES, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { useLocalize } from '~/context/localize' import { loadShouts } from '~/graphql/api/public' -import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' -import { LayoutType } from '~/types/common' +import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' +import { ExpoLayoutType } from '~/types/common' +import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' +import { byCreated } from '~/utils/sort' const fetchExpoShouts = async (layouts: string[]) => { const result = await loadShouts({ @@ -28,35 +32,50 @@ export const route = { export default (props: RouteSectionProps) => { const { t } = useLocalize() + const { expoFeed, setExpoFeed, feedByLayout } = useFeed() + const [loadMoreVisible, setLoadMoreVisible] = createSignal(false) + const getTitle = (l?: string) => EXPO_TITLES[(l as ExpoLayoutType) || ''] + const shouts = createAsync( async () => props.data || (await fetchExpoShouts(props.params.layout ? [props.params.layout] : EXPO_LAYOUTS)) ) - const getTitle = (l: string) => { - switch (l) { - case 'audio': { - return t('Audio') - } - case 'video': { - return t('Video') - } - case 'image': { - return t('Artworks') - } - case 'literature': { - return t('Literature') - } - default: { - return t('Art') - } + // Функция для загрузки дополнительных шотов + const loadMore = async () => { + saveScrollPosition() + const limit = SHOUTS_PER_PAGE + const layouts = props.params.layout ? [props.params.layout] : EXPO_LAYOUTS + const offset = expoFeed()?.length || 0 + const filters: LoadShoutsFilters = { layouts, featured: true } + const options: LoadShoutsOptions = { filters, limit, offset } + const shoutsFetcher = loadShouts(options) + const result = await shoutsFetcher() + setLoadMoreVisible(Boolean(result?.length)) + if (result) { + setExpoFeed((prev) => Array.from(new Set([...(prev || []), ...result])).sort(byCreated)) } + restoreScrollPosition() + return result as LoadMoreItems } - - onMount(() => { - document.title = getTitle(props.params.layout || '') - }) - + // Эффект для загрузки данных при изменении layout + createEffect( + on( + () => props.params.layout as ExpoLayoutType, + async (layout?: ExpoLayoutType) => { + const layouts = layout ? [layout] : EXPO_LAYOUTS + const offset = (layout ? feedByLayout()[layout]?.length : expoFeed()?.length) || 0 + const options: LoadShoutsOptions = { + filters: { layouts, featured: true }, + limit: SHOUTS_PER_PAGE, + offset + } + const shoutsFetcher = loadShouts(options) + const result = await shoutsFetcher() + setExpoFeed(result || []) + } + ) + ) return ( ) => { title={`${t('Discours')} :: ${getTitle(props.params.layout || '')}`} > - - {(sss) => } - + + ) } diff --git a/src/types/common.d.ts b/src/types/common.d.ts index 8e0f882b..2911baf2 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -4,7 +4,8 @@ export type RootSearchParams = { token: string; }; -export type LayoutType = 'article' | 'audio' | 'video' | 'image' | 'literature'; +export type ExpoLayoutType = 'audio' | 'video' | 'image' | 'literature'; +export type LayoutType = 'article' | ExpoLayoutType; export type FollowsFilter = 'all' | 'authors' | 'topics' | 'communities'; export type SortFunction = (a: T, b: T) => number export type FilterFunction = (a: T) => boolean From 0c6144529338169a640dab41afd34f13123e2147 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 10:51:51 +0300 Subject: [PATCH 12/23] config-fix --- src/config.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/config.ts b/src/config.ts index f34c0972..207e843d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,7 +11,4 @@ export const authorizerRedirectUrl = import.meta.env.PUBLIC_AUTHORIZER_REDIRECT_URL || 'https://testing.discours.io' // devmode only -export const isDev = import.meta.env.MODE === 'development' -export const reportDsn = isDev - ? import.meta.env.PUBLIC_GLITCHTIP_DSN || import.meta.env.PUBLIC_SENTRY_DSN || '' - : '' +export const reportDsn = import.meta.env.PUBLIC_GLITCHTIP_DSN From 962140e755012af80237dd772fb56497683070f7 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 16:46:43 +0300 Subject: [PATCH 13/23] microeditor-wip --- .../Editor/InlineForm/InlineForm.tsx | 22 ++- .../Editor/InsertLinkForm/InsertLinkForm.tsx | 26 ++- .../MicroEditor/MicroEditor.stories.tsx | 51 ++++++ .../Editor/MicroEditor/MicroEditor.tsx | 173 ++++++++++++++++++ src/context/feed.tsx | 8 +- src/lib/editorExtensions.ts | 57 ++---- 6 files changed, 275 insertions(+), 62 deletions(-) create mode 100644 src/components/Editor/MicroEditor/MicroEditor.stories.tsx create mode 100644 src/components/Editor/MicroEditor/MicroEditor.tsx diff --git a/src/components/Editor/InlineForm/InlineForm.tsx b/src/components/Editor/InlineForm/InlineForm.tsx index 2eaf983f..4325832f 100644 --- a/src/components/Editor/InlineForm/InlineForm.tsx +++ b/src/components/Editor/InlineForm/InlineForm.tsx @@ -1,5 +1,5 @@ import { clsx } from 'clsx' -import { createSignal, onMount } from 'solid-js' +import { createEffect, createSignal, onMount } from 'solid-js' import { Icon } from '~/components/_shared/Icon' import { Popover } from '~/components/_shared/Popover' @@ -15,20 +15,24 @@ type Props = { initialValue?: string showInput?: boolean placeholder: string + onFocus?: (event: FocusEvent) => void } export const InlineForm = (props: Props) => { const { t } = useLocalize() const [formValue, setFormValue] = createSignal(props.initialValue || '') const [formValueError, setFormValueError] = createSignal() - - let inputRef: HTMLInputElement | undefined + const [inputRef, setInputRef] = createSignal() const handleFormInput = (e: { currentTarget: HTMLInputElement; target: HTMLInputElement }) => { const value = (e.currentTarget || e.target).value setFormValueError() setFormValue(value) } + createEffect(() => { + setFormValue(props.initialValue || '') + }) + const handleSaveButtonClick = async () => { if (props.validate) { const errorMessage = await props.validate(formValue()) @@ -56,23 +60,23 @@ export const InlineForm = (props: Props) => { } const handleClear = () => { - props.initialValue ? props.onClear?.() : props.onClose() + props.initialValue && props.onClear?.() + props.onClose() } - onMount(() => { - inputRef?.focus() - }) + onMount(() => inputRef()?.focus()) return (
(inputRef = el)} + ref={setInputRef} type="text" - value={props.initialValue ?? ''} + value={formValue()} placeholder={props.placeholder} onKeyDown={handleKeyDown} onInput={handleFormInput} + onFocus={props.onFocus} /> {(triggerRef: (el: HTMLElement) => void) => ( diff --git a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx index 379c3f5d..601ad0ec 100644 --- a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx +++ b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx @@ -1,5 +1,5 @@ import { Editor } from '@tiptap/core' -import { createEditorTransaction } from 'solid-tiptap' +import { createEffect, createSignal, onCleanup } from 'solid-js' import { useLocalize } from '~/context/localize' import { validateUrl } from '~/utils/validate' @@ -8,6 +8,7 @@ import { InlineForm } from '../InlineForm' type Props = { editor: Editor onClose: () => void + onFocus: (event: FocusEvent) => void } export const checkUrl = (url: string) => { @@ -21,12 +22,22 @@ export const checkUrl = (url: string) => { export const InsertLinkForm = (props: Props) => { const { t } = useLocalize() - const currentUrl = createEditorTransaction( - () => props.editor, - (ed) => { - return ed?.getAttributes('link').href || '' + const [currentUrl, setCurrentUrl] = createSignal('') + + createEffect(() => { + const url = props.editor.getAttributes('link').href + setCurrentUrl(url || '') + }) + + createEffect(() => { + const updateListener = () => { + const url = props.editor.getAttributes('link').href + setCurrentUrl(url || '') } - ) + props.editor.on('update', updateListener) + onCleanup(() => props.editor.off('update', updateListener)) + }) + const handleClearLinkForm = () => { if (currentUrl()) { props.editor?.chain().focus().unsetLink().run() @@ -39,7 +50,9 @@ export const InsertLinkForm = (props: Props) => { .focus() .setLink({ href: checkUrl(value) }) .run() + props.onClose() } + return (
{ validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))} onSubmit={handleLinkFormSubmit} onClose={props.onClose} + onFocus={props.onFocus} />
) diff --git a/src/components/Editor/MicroEditor/MicroEditor.stories.tsx b/src/components/Editor/MicroEditor/MicroEditor.stories.tsx new file mode 100644 index 00000000..da31bd3f --- /dev/null +++ b/src/components/Editor/MicroEditor/MicroEditor.stories.tsx @@ -0,0 +1,51 @@ +import { Meta, StoryObj } from 'storybook-solidjs' +import { MicroEditor } from './MicroEditor' + +const meta: Meta = { + title: 'Components/MicroEditor', + component: MicroEditor, + argTypes: { + content: { + control: 'text', + description: 'Initial content for the editor', + defaultValue: '' + }, + 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: '', + placeholder: 'Start typing here...', + onChange: (content: string) => console.log('Content changed:', content) + } +} + +export const WithInitialContent: Story = { + args: { + content: 'This is some initial content.', + placeholder: 'Start typing here...', + onChange: (content: string) => console.log('Content changed:', content) + } +} + +export const WithCustomPlaceholder: Story = { + args: { + content: '', + placeholder: 'Type your text here...', + onChange: (content: string) => console.log('Content changed:', content) + } +} diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx new file mode 100644 index 00000000..e16fcf33 --- /dev/null +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -0,0 +1,173 @@ +import type { Editor } from '@tiptap/core' +import Placeholder from '@tiptap/extension-placeholder' +import clsx from 'clsx' +import { type JSX, Show, createEffect, createReaction, createSignal, on, onCleanup } from 'solid-js' +import { + createEditorTransaction, + createTiptapEditor, + useEditorHTML, + useEditorIsEmpty, + useEditorIsFocused +} from 'solid-tiptap' +import { Icon } from '~/components/_shared/Icon/Icon' +import { Popover } from '~/components/_shared/Popover/Popover' +import { useLocalize } from '~/context/localize' +import { minimal } from '~/lib/editorExtensions' +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 MicroEditorProps { + content?: string + onChange?: (content: string) => void + placeholder?: string +} + +const prevent = (e: Event) => e.preventDefault() + +export const MicroEditor = (props: MicroEditorProps): JSX.Element => { + const { t } = useLocalize() + const [editorElement, setEditorElement] = createSignal() + const [showLinkInput, setShowLinkInput] = createSignal(false) + const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) + const [toolbarElement, setToolbarElement] = createSignal() + const [selectionRange, setSelectionRange] = createSignal(null) + + const handleLinkInputFocus = (event: FocusEvent) => { + event.preventDefault() + const selection = window.getSelection() + if (selection?.rangeCount) { + setSelectionRange(selection.getRangeAt(0)) + } + } + + const editor = createTiptapEditor(() => ({ + element: editorElement()!, + extensions: [ + ...minimal, + Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }) + ], + 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(on([isTextSelection, showLinkInput],([selected, linkEditing]) => !linkEditing && setShowSimpleMenu(selected))) + createEffect(on(html, (c?: string) => c && props.onChange?.(c))) + createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run())) + createReaction(on(toolbarElement, (t?: HTMLElement) => t?.addEventListener('mousedown', prevent))) + onCleanup(() => toolbarElement()?.removeEventListener('mousedown', prevent)) + + return ( +
+
+ + {(instance) => ( + +
+
+ { + setShowLinkInput(false) + if (selectionRange()) { + const selection = window.getSelection() + selection?.removeAllRanges() + selection?.addRange(selectionRange()!) + } + }} + onFocus={handleLinkInputFocus} />} + > +
+ instance.chain().focus().toggleBold().run()} + title={t('Bold')} + > + + + instance.chain().focus().toggleItalic().run()} + title={t('Italic')} + > + + + setShowLinkInput(!showLinkInput())} + title={t('Add url')} + isActive={showLinkInput} + > + + +
+
+
+
+
+ )} +
+ +
+
+
+ ) +} + +export default MicroEditor diff --git a/src/context/feed.tsx b/src/context/feed.tsx index b6d22375..43508628 100644 --- a/src/context/feed.tsx +++ b/src/context/feed.tsx @@ -18,10 +18,10 @@ export const PRERENDERED_ARTICLES_COUNT = 5 export const SHOUTS_PER_PAGE = 20 export const EXPO_LAYOUTS = ['audio', 'literature', 'video', 'image'] as ExpoLayoutType[] export const EXPO_TITLES: Record = { - 'audio': 'Audio', - 'video': 'Video', - 'image': 'Artworks', - 'literature': 'Literature', + audio: 'Audio', + video: 'Video', + image: 'Artworks', + literature: 'Literature', '': 'All' } diff --git a/src/lib/editorExtensions.ts b/src/lib/editorExtensions.ts index b6603987..8e6553c5 100644 --- a/src/lib/editorExtensions.ts +++ b/src/lib/editorExtensions.ts @@ -1,4 +1,6 @@ import { EditorOptions } from '@tiptap/core' +import Bold from '@tiptap/extension-bold' +import { Document as DocExt } from '@tiptap/extension-document' import Dropcursor from '@tiptap/extension-dropcursor' import Focus from '@tiptap/extension-focus' import Gapcursor from '@tiptap/extension-gapcursor' @@ -6,7 +8,10 @@ import HardBreak from '@tiptap/extension-hard-break' import Highlight from '@tiptap/extension-highlight' import HorizontalRule from '@tiptap/extension-horizontal-rule' import Image from '@tiptap/extension-image' +import Italic from '@tiptap/extension-italic' import Link from '@tiptap/extension-link' +import Paragraph from '@tiptap/extension-paragraph' +import { Text } from '@tiptap/extension-text' import Underline from '@tiptap/extension-underline' import StarterKit from '@tiptap/starter-kit' import ArticleNode from '~/components/Editor/extensions/Article' @@ -42,6 +47,15 @@ export const base: EditorOptions['extensions'] = [ }) ] +export const minimal: EditorOptions['extensions'] = [ + DocExt, + Text, + Paragraph, + Bold, + Italic, + Link.configure({ autolink: true, openOnClick: false }) +] + // Extend the Figure extension to include Figcaption export const ImageFigure = Figure.extend({ name: 'capturedImage', @@ -71,46 +85,3 @@ export const extended: EditorOptions['extensions'] = [ HardBreak, ArticleNode ] - -/* - 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.') - } -} -*/ From 7aa01d615204f3fe223f878ef427a90080a3a637 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 17:26:40 +0300 Subject: [PATCH 14/23] MiniEditor-fix --- .../Editor/InsertLinkForm/InsertLinkForm.tsx | 2 - .../Editor/MicroEditor/MicroEditor.tsx | 112 ++++++++------- .../Editor/MiniEditor/MiniEditor.tsx | 134 ++++++++---------- 3 files changed, 121 insertions(+), 127 deletions(-) diff --git a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx index 601ad0ec..825be6ff 100644 --- a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx +++ b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx @@ -8,7 +8,6 @@ import { InlineForm } from '../InlineForm' type Props = { editor: Editor onClose: () => void - onFocus: (event: FocusEvent) => void } export const checkUrl = (url: string) => { @@ -62,7 +61,6 @@ export const InsertLinkForm = (props: Props) => { validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))} onSubmit={handleLinkFormSubmit} onClose={props.onClose} - onFocus={props.onFocus} />
) diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index e16fcf33..48233335 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -63,15 +63,6 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { const [showLinkInput, setShowLinkInput] = createSignal(false) const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) const [toolbarElement, setToolbarElement] = createSignal() - const [selectionRange, setSelectionRange] = createSignal(null) - - const handleLinkInputFocus = (event: FocusEvent) => { - event.preventDefault() - const selection = window.getSelection() - if (selection?.rangeCount) { - setSelectionRange(selection.getRangeAt(0)) - } - } const editor = createTiptapEditor(() => ({ element: editorElement()!, @@ -87,12 +78,32 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { content: props.content || '' })) + const selection = createEditorTransaction(editor, (instance) => instance?.state.selection) + const [storedSelection, setStoredSelection] = createSignal() + const recoverSelection = () => { + if (!storedSelection()?.empty) { + // TODO set selection range from stored + createEditorTransaction(editor, (instance?: Editor) => { + const r = selection() + if (instance && r) { + instance.state.selection.from === r.from + instance.state.selection.to === r.to + } + }) + } + } + const storeSelection = (event: Event) => { + event.preventDefault() + const selection = editor()?.state.selection + if (!selection?.empty) { + setStoredSelection(selection) + } + } + const isEmpty = useEditorIsEmpty(editor) const isFocused = useEditorIsFocused(editor) - const isTextSelection = createEditorTransaction(editor, (instance) => !instance?.state.selection.empty) const html = useEditorHTML(editor) - - createEffect(on([isTextSelection, showLinkInput],([selected, linkEditing]) => !linkEditing && setShowSimpleMenu(selected))) + createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty))) createEffect(on(html, (c?: string) => c && props.onChange?.(c))) createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run())) createReaction(on(toolbarElement, (t?: HTMLElement) => t?.addEventListener('mousedown', prevent))) @@ -117,46 +128,41 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { ref={setToolbarElement} >
- { - setShowLinkInput(false) - if (selectionRange()) { - const selection = window.getSelection() - selection?.removeAllRanges() - selection?.addRange(selectionRange()!) - } - }} - onFocus={handleLinkInputFocus} />} - > -
- instance.chain().focus().toggleBold().run()} - title={t('Bold')} - > - - - instance.chain().focus().toggleItalic().run()} - title={t('Italic')} - > - - - setShowLinkInput(!showLinkInput())} - title={t('Add url')} - isActive={showLinkInput} - > - - -
+
+ instance.chain().focus().toggleBold().run()} + title={t('Bold')} + > + + + instance.chain().focus().toggleItalic().run()} + title={t('Italic')} + > + + + setShowLinkInput(!showLinkInput())} + title={t('Add url')} + isActive={showLinkInput} + > + + +
+ + { + setShowLinkInput(false) + recoverSelection() + }} + />
@@ -164,7 +170,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { )} -
+
) diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index 011d951e..1be9af48 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -2,13 +2,10 @@ 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 { type JSX, Show, createEffect, createSignal, on, onCleanup } from 'solid-js' import { - createEditorTransaction, createTiptapEditor, useEditorHTML, - useEditorIsEmpty, - useEditorIsFocused } from 'solid-tiptap' import { Toolbar } from 'terracotta' import { Icon } from '~/components/_shared/Icon/Icon' @@ -63,7 +60,6 @@ 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() @@ -82,12 +78,9 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { 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(on(html, (c?: string) => c && props.onChange?.(c))) + createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run())) createEffect(() => { const textLength = editor()?.getText().length || 0 @@ -112,77 +105,74 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { }) 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')} - > - - -
-
-
- )} -
-
-
-
+ + + {(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 || '∞'} +
) From 90057a2d0e83450e0954ccd56d8506f58acba89c Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 17:27:01 +0300 Subject: [PATCH 15/23] fmt --- src/components/Editor/MiniEditor/MiniEditor.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index 1be9af48..4393f0b4 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -3,10 +3,7 @@ import CharacterCount from '@tiptap/extension-character-count' import Placeholder from '@tiptap/extension-placeholder' import clsx from 'clsx' import { type JSX, Show, createEffect, createSignal, on, onCleanup } from 'solid-js' -import { - createTiptapEditor, - useEditorHTML, -} from 'solid-tiptap' +import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { Toolbar } from 'terracotta' import { Icon } from '~/components/_shared/Icon/Icon' import { Popover } from '~/components/_shared/Popover/Popover' @@ -104,13 +101,15 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { toolbarElement()?.removeEventListener('mousedown', handleMouseDownOnToolbar) }) return ( -
+
- + {(instance) => (
@@ -172,7 +171,6 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { {counter()} / {props.limit || '∞'} -
) From b393810f7a6d8fd1b6348d6d0b08b4a42ebeee92 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 17:28:50 +0300 Subject: [PATCH 16/23] cleanup-stories --- .../Editor/SimplifiedEditor.stories.tsx | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 src/components/Editor/SimplifiedEditor.stories.tsx diff --git a/src/components/Editor/SimplifiedEditor.stories.tsx b/src/components/Editor/SimplifiedEditor.stories.tsx deleted file mode 100644 index 2242bd43..00000000 --- a/src/components/Editor/SimplifiedEditor.stories.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Meta, StoryObj } from 'storybook-solidjs' -import SimplifiedEditor from './SimplifiedEditor' - -const meta: Meta = { - title: 'Components/SimplifiedEditor', - component: SimplifiedEditor, - argTypes: { - placeholder: { - control: 'text', - description: 'Placeholder text when the editor is empty', - defaultValue: 'Type something...' - }, - initialContent: { - control: 'text', - description: 'Initial content for the editor', - defaultValue: '' - }, - maxLength: { - control: 'number', - description: 'Character limit for the editor', - defaultValue: 400 - }, - quoteEnabled: { - control: 'boolean', - description: 'Whether the blockquote feature is enabled', - defaultValue: true - }, - imageEnabled: { - control: 'boolean', - description: 'Whether the image feature is enabled', - defaultValue: true - }, - submitButtonText: { - control: 'text', - description: 'Text for the submit button', - defaultValue: 'Submit' - }, - onSubmit: { - action: 'submitted', - description: 'Callback when the form is submitted' - }, - onCancel: { - action: 'cancelled', - description: 'Callback when the editor is cleared' - }, - onChange: { - action: 'changed', - description: 'Callback when the content changes' - } - } -} - -export default meta - -type Story = StoryObj - -export const Default: Story = { - args: { - placeholder: 'Type something...', - initialContent: '', - maxLength: 400, - quoteEnabled: true, - imageEnabled: true, - submitButtonText: 'Submit' - } -} - -export const WithInitialContent: Story = { - args: { - placeholder: 'Type something...', - initialContent: 'This is some initial content', - maxLength: 400, - quoteEnabled: true, - imageEnabled: true, - submitButtonText: 'Submit' - } -} - -export const WithCharacterLimit: Story = { - args: { - placeholder: 'You have a 50 character limit...', - initialContent: '', - maxLength: 50, - quoteEnabled: true, - imageEnabled: true, - submitButtonText: 'Submit' - } -} - -export const WithCustomPlaceholder: Story = { - args: { - placeholder: 'Custom placeholder here...', - initialContent: '', - maxLength: 400, - quoteEnabled: true, - imageEnabled: true, - submitButtonText: 'Submit' - } -} From 595e2b8a4b3486abf72facd0a8fbd1e0728b6cdd Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 19:31:54 +0300 Subject: [PATCH 17/23] editor-refactoring --- .../Article/AudioPlayer/PlayerPlaylist.tsx | 7 +- src/components/Article/Comment/Comment.tsx | 19 +-- src/components/Article/CommentsTree.tsx | 18 +- .../Editor/BubbleMenu/FigureBubbleMenu.tsx | 11 +- src/components/Editor/Editor.stories.tsx | 154 ++++-------------- src/components/Editor/Editor.tsx | 2 +- .../EditorFloatingMenu/EditorFloatingMenu.tsx | 7 +- .../Editor/EditorToolbar/MicroToolbar.tsx | 113 +++++++++++++ .../Editor/EditorToolbar/MiniToolbar.tsx | 117 +++++++++++++ .../SimplifiedToolbar.tsx} | 12 +- .../Editor/EditorToolbar/ToolbarControl.tsx | 40 +++++ .../Editor/InsertLinkForm/InsertLinkForm.tsx | 2 +- .../Editor/MicroEditor/MicroEditor.tsx | 137 +--------------- .../Editor/MiniEditor/MiniEditor.tsx | 127 +-------------- src/components/Editor/SimplifiedEditor.tsx | 8 +- .../Editor/TextBubbleMenu/TextBubbleMenu.tsx | 13 +- src/components/Editor/index.ts | 4 +- .../TopicSelect/TopicSelect.module.scss | 0 .../{Editor => }/TopicSelect/TopicSelect.tsx | 0 .../{Editor => }/TopicSelect/index.ts | 0 .../AudioUploader/AudioUploader.module.scss | 0 .../AudioUploader/AudioUploader.tsx | 0 .../{Editor => Upload}/AudioUploader/index.ts | 0 .../UploadModalContent.module.scss | 0 .../UploadModalContent/UploadModalContent.tsx | 2 +- .../UploadModalContent/index.ts | 0 .../VideoUploader/VideoUploader.module.scss | 0 .../VideoUploader/VideoUploader.tsx | 0 .../{Editor => Upload}/VideoUploader/index.ts | 0 .../{Editor => Upload}/renderUploadedImage.ts | 0 src/components/Views/EditView/EditView.tsx | 15 +- src/components/Views/Inbox/Inbox.tsx | 18 +- .../Views/Profile/ProfileSettings.tsx | 16 +- .../Views/PublishSettings/PublishSettings.tsx | 14 +- .../InlineForm/InlineForm.module.scss | 0 .../InlineForm/InlineForm.tsx | 0 .../{Editor => _shared}/InlineForm/index.ts | 0 .../_shared/SolidSwiper/EditorSwiper.tsx | 8 +- src/entry-client.tsx | 2 + 39 files changed, 382 insertions(+), 484 deletions(-) create mode 100644 src/components/Editor/EditorToolbar/MicroToolbar.tsx create mode 100644 src/components/Editor/EditorToolbar/MiniToolbar.tsx rename src/components/Editor/{EditorToolbar.tsx => EditorToolbar/SimplifiedToolbar.tsx} (93%) create mode 100644 src/components/Editor/EditorToolbar/ToolbarControl.tsx rename src/components/{Editor => }/TopicSelect/TopicSelect.module.scss (100%) rename src/components/{Editor => }/TopicSelect/TopicSelect.tsx (100%) rename src/components/{Editor => }/TopicSelect/index.ts (100%) rename src/components/{Editor => Upload}/AudioUploader/AudioUploader.module.scss (100%) rename src/components/{Editor => Upload}/AudioUploader/AudioUploader.tsx (100%) rename src/components/{Editor => Upload}/AudioUploader/index.ts (100%) rename src/components/{Editor => Upload}/UploadModalContent/UploadModalContent.module.scss (100%) rename src/components/{Editor => Upload}/UploadModalContent/UploadModalContent.tsx (98%) rename src/components/{Editor => Upload}/UploadModalContent/index.ts (100%) rename src/components/{Editor => Upload}/VideoUploader/VideoUploader.module.scss (100%) rename src/components/{Editor => Upload}/VideoUploader/VideoUploader.tsx (100%) rename src/components/{Editor => Upload}/VideoUploader/index.ts (100%) rename src/components/{Editor => Upload}/renderUploadedImage.ts (100%) rename src/components/{Editor => _shared}/InlineForm/InlineForm.module.scss (100%) rename src/components/{Editor => _shared}/InlineForm/InlineForm.tsx (100%) rename src/components/{Editor => _shared}/InlineForm/index.ts (100%) diff --git a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx index 10a740a7..af0e0e5b 100644 --- a/src/components/Article/AudioPlayer/PlayerPlaylist.tsx +++ b/src/components/Article/AudioPlayer/PlayerPlaylist.tsx @@ -9,7 +9,7 @@ import { SharePopup, getShareUrl } from '../SharePopup' import styles from './AudioPlayer.module.scss' -const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) +const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor')) const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea')) type Props = { @@ -171,10 +171,9 @@ export const PlayerPlaylist = (props: Props) => { } >
- handleMediaItemFieldChange('body', value)} /> import('../../Editor/SimplifiedEditor')) +const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor')) type Props = { comment: Reaction @@ -41,7 +41,6 @@ export const Comment = (props: Props) => { const [isReplyVisible, setIsReplyVisible] = createSignal(false) const [loading, setLoading] = createSignal(false) const [editMode, setEditMode] = createSignal(false) - const [clearEditor, setClearEditor] = createSignal(false) const [editedBody, setEditedBody] = createSignal() const { session, client } = useSession() const author = createMemo(() => session()?.user?.app_data?.profile as Author) @@ -104,13 +103,11 @@ export const Comment = (props: Props) => { shout: props.comment.shout.id } } as MutationCreate_ReactionArgs) - setClearEditor(true) setIsReplyVisible(false) setLoading(false) } catch (error) { console.error('[handleCreate reaction]:', error) } - setClearEditor(false) } const toggleEditMode = () => { @@ -189,16 +186,11 @@ export const Comment = (props: Props) => {
}> {t('Loading')}

}> - handleUpdate(value)} - submitByCtrlEnter={true} onCancel={() => setEditMode(false)} - setClear={clearEditor()} />
@@ -258,12 +250,9 @@ export const Comment = (props: Props) => { {t('Loading')}

}> - handleCreate(value)} - submitByCtrlEnter={true} />
diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index e53a9f39..ca3b9e43 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -9,11 +9,12 @@ import { Author, Reaction, ReactionKind, ReactionSort } from '~/graphql/schema/c import { SortFunction } from '~/types/common' import { byCreated, byStat } from '~/utils/sort' import { Button } from '../_shared/Button' +import { Loading } from '../_shared/Loading' import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import styles from './Article.module.scss' import { Comment } from './Comment' -const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) +const MiniEditor = lazy(() => import('../Editor/MiniEditor/MiniEditor')) type Props = { articleAuthors: Author[] @@ -27,7 +28,6 @@ export const CommentsTree = (props: Props) => { const [commentsOrder, setCommentsOrder] = createSignal(ReactionSort.Newest) const [onlyNew, setOnlyNew] = createSignal(false) const [newReactions, setNewReactions] = createSignal([]) - const [clearEditor, setClearEditor] = createSignal(false) const [clickedReplyId, setClickedReplyId] = createSignal() const { reactionEntities, createShoutReaction, loadReactionsBy } = useReactions() @@ -70,6 +70,7 @@ export const CommentsTree = (props: Props) => { setCookie() } }) + const [posting, setPosting] = createSignal(false) const handleSubmitComment = async (value: string) => { setPosting(true) @@ -81,12 +82,10 @@ export const CommentsTree = (props: Props) => { shout: props.shoutId } }) - setClearEditor(true) await loadReactionsBy({ by: { shout: props.shoutSlug } }) } catch (error) { console.error('[handleCreate reaction]:', error) } - setClearEditor(false) setPosting(false) } @@ -155,16 +154,13 @@ export const CommentsTree = (props: Props) => {
} > - handleSubmitComment(value)} - setClear={clearEditor()} - isPosting={posting()} /> + + + ) diff --git a/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx index 706c9048..2ac02b7a 100644 --- a/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx @@ -1,14 +1,13 @@ import type { Editor } from '@tiptap/core' - -import { renderUploadedImage } from '~/components/Editor/renderUploadedImage' +import { renderUploadedImage } from '~/components/Upload/renderUploadedImage' import { Icon } from '~/components/_shared/Icon' import { Popover } from '~/components/_shared/Popover' import { useLocalize } from '~/context/localize' -import { UploadedFile } from '~/types/upload' -import { Modal } from '../../_shared/Modal' -import { UploadModalContent } from '../UploadModalContent' - import { useUI } from '~/context/ui' +import { UploadedFile } from '~/types/upload' +import { UploadModalContent } from '../../Upload/UploadModalContent' +import { Modal } from '../../_shared/Modal' + import styles from './BubbleMenu.module.scss' type Props = { diff --git a/src/components/Editor/Editor.stories.tsx b/src/components/Editor/Editor.stories.tsx index e7d0f255..3203bf14 100644 --- a/src/components/Editor/Editor.stories.tsx +++ b/src/components/Editor/Editor.stories.tsx @@ -1,105 +1,28 @@ -import { Editor, EditorOptions } from '@tiptap/core' -import { createSignal } from 'solid-js' -import { createStore } from 'solid-js/store' import { Meta, StoryObj } from 'storybook-solidjs' -import { EditorContext, EditorContextType, ShoutForm } from '~/context/editor' -import { LocalizeContext, LocalizeContextType } from '~/context/localize' -import { SessionContext, SessionContextType } from '~/context/session' -import { SnackbarContext, SnackbarContextType } from '~/context/ui' -import { EditorComponent, EditorComponentProps } from './Editor' - -// Mock data -const mockSession = { - session: () => ({ - user: { - app_data: { - profile: { - name: 'Test User', - slug: 'test-user' - } - } - }, - access_token: 'mock-access-token' - }) -} - -const mockLocalize = { - t: (key: string) => key, - lang: () => 'en' -} - -const [_form, setForm] = createStore({ - body: '', - slug: '', - shoutId: 0, - title: '', - selectedTopics: [] -}) -const [_formErrors, setFormErrors] = createStore({} as Record) -const [editor, setEditor] = createSignal() - -const mockEditorContext: EditorContextType = { - countWords: () => 0, - isEditorPanelVisible: () => false, - wordCounter: () => ({ characters: 0, words: 0 }), - form: _form, - formErrors: _formErrors, - createEditor: (opts?: Partial) => { - const newEditor = new Editor(opts) - setEditor(newEditor) - return newEditor - }, - editor, - saveShout: async (_form: ShoutForm) => { - // Simulate save - }, - saveDraft: async (_form: ShoutForm) => { - // Simulate save draft - }, - saveDraftToLocalStorage: (_form: ShoutForm) => { - // Simulate save to local storage - }, - getDraftFromLocalStorage: (_shoutId: number): ShoutForm => _form, - publishShout: async (_form: ShoutForm) => { - // Simulate publish - }, - publishShoutById: async (_shoutId: number) => { - // Simulate publish by ID - }, - deleteShout: async (_shoutId: number): Promise => true, - toggleEditorPanel: () => { - // Simulate toggle - }, - setForm, - setFormErrors -} - -const mockSnackbarContext = { - showSnackbar: console.log -} +import { EditorComponent } from './Editor' const meta: Meta = { title: 'Components/Editor', component: EditorComponent, argTypes: { - shoutId: { - control: 'number', - description: 'Unique identifier for the shout (document)', - defaultValue: 1 - }, - initialContent: { + content: { control: 'text', description: 'Initial content for the editor', defaultValue: '' }, - onChange: { - action: 'contentChanged', - description: 'Callback when the content changes' + limit: { + control: 'number', + description: 'Character limit for the editor', + defaultValue: 500 }, - disableCollaboration: { - control: 'boolean', - description: 'Disable collaboration features for Storybook', - defaultValue: true + placeholder: { + control: 'text', + description: 'Placeholder text when the editor is empty', + defaultValue: 'Start typing here...' + }, + onChange: { + action: 'changed', + description: 'Callback when the content changes' } } } @@ -109,38 +32,33 @@ export default meta type Story = StoryObj export const Default: Story = { - render: (props: EditorComponentProps) => { - const [_content, setContent] = createSignal(props.initialContent || '') - - return ( - - - - - { - props.onChange(text) - setContent(text) - }} - /> - - - - - ) - }, args: { - shoutId: 1, - initialContent: '', - disableCollaboration: true + content: '', + limit: 500, + placeholder: 'Start typing here...' } } export const WithInitialContent: Story = { - ...Default, args: { - ...Default.args, - initialContent: '

This is some initial content in the editor.

' + 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/Editor.tsx b/src/components/Editor/Editor.tsx index 04879023..4a05a420 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -16,10 +16,10 @@ import { useSnackbar } from '~/context/ui' import { Author } from '~/graphql/schema/core.gen' import { base, custom, extended } from '~/lib/editorExtensions' import { handleImageUpload } from '~/lib/handleImageUpload' +import { renderUploadedImage } from '../Upload/renderUploadedImage' import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' import { TextBubbleMenu } from './TextBubbleMenu' -import { renderUploadedImage } from './renderUploadedImage' import './Prosemirror.scss' diff --git a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx index c455a053..b2a2876a 100644 --- a/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx +++ b/src/components/Editor/EditorFloatingMenu/EditorFloatingMenu.tsx @@ -1,15 +1,14 @@ import type { Editor } from '@tiptap/core' import { Show, createEffect, createSignal } from 'solid-js' - -import { renderUploadedImage } from '~/components/Editor/renderUploadedImage' +import { renderUploadedImage } from '~/components/Upload/renderUploadedImage' import { Icon } from '~/components/_shared/Icon' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' import { useOutsideClickHandler } from '~/lib/useOutsideClickHandler' import { UploadedFile } from '~/types/upload' +import { UploadModalContent } from '../../Upload/UploadModalContent' +import { InlineForm } from '../../_shared/InlineForm' import { Modal } from '../../_shared/Modal' -import { InlineForm } from '../InlineForm' -import { UploadModalContent } from '../UploadModalContent' import { Menu } from './Menu' import type { MenuItem } from './Menu/Menu' diff --git a/src/components/Editor/EditorToolbar/MicroToolbar.tsx b/src/components/Editor/EditorToolbar/MicroToolbar.tsx new file mode 100644 index 00000000..8e9b42db --- /dev/null +++ b/src/components/Editor/EditorToolbar/MicroToolbar.tsx @@ -0,0 +1,113 @@ +import { Editor } from '@tiptap/core' +import { Show, createEffect, createSignal, on } from 'solid-js' +import { createEditorTransaction } from 'solid-tiptap' +import { Icon } from '~/components/_shared/Icon/Icon' +import { useLocalize } from '~/context/localize' +import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' +import { ToolbarControl as Control } from './ToolbarControl' + +import styles from '../SimplifiedEditor.module.scss' + +export interface MicroToolbarProps { + showing?: boolean + editor?: Editor +} + +export const MicroToolbar = (props: MicroToolbarProps) => { + const { t } = useLocalize() + + // show / hide for menu + const [showSimpleMenu, setShowSimpleMenu] = createSignal(!props.showing) + const selection = createEditorTransaction( + () => props.editor, + (instance) => instance?.state.selection + ) + + // show / hide for link input + const [showLinkInput, setShowLinkInput] = createSignal(false) + + // change visibility on selection if not in link input mode + createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty))) + + // focus on link input when it shows up + createEffect(on(showLinkInput, (x?: boolean) => x && props.editor?.chain().focus().run())) + + const [storedSelection, setStoredSelection] = createSignal() + const recoverSelection = () => { + if (!storedSelection()?.empty) { + createEditorTransaction( + () => props.editor, + (instance?: Editor) => { + const r = selection() + if (instance && r) { + instance.state.selection.from === r.from + instance.state.selection.to === r.to + } + } + ) + } + } + const storeSelection = () => { + const selection = props.editor?.state.selection + if (!selection?.empty) { + setStoredSelection(selection) + } + } + const toggleShowLink = () => { + if (showLinkInput()) { + props.editor?.chain().focus().run() + recoverSelection() + } else { + storeSelection() + } + setShowLinkInput(!showLinkInput()) + } + return ( + + {(instance) => ( + +
+
+
+ instance.chain().focus().toggleBold().run()} + title={t('Bold')} + > + + + instance.chain().focus().toggleItalic().run()} + title={t('Italic')} + > + + + + + +
+ + + +
+
+
+ )} +
+ ) +} diff --git a/src/components/Editor/EditorToolbar/MiniToolbar.tsx b/src/components/Editor/EditorToolbar/MiniToolbar.tsx new file mode 100644 index 00000000..e4e7f036 --- /dev/null +++ b/src/components/Editor/EditorToolbar/MiniToolbar.tsx @@ -0,0 +1,117 @@ +import { Editor } from '@tiptap/core' +import { Show, createEffect, createSignal, on } from 'solid-js' +import { Icon } from '~/components/_shared/Icon/Icon' +import { useLocalize } from '~/context/localize' +import { useUI } from '~/context/ui' +import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' +import { ToolbarControl as Control } from './ToolbarControl' + +import { createEditorTransaction } from 'solid-tiptap' +import styles from '../SimplifiedEditor.module.scss' + +interface MiniToolbarProps { + editor?: Editor +} + +export const MiniToolbar = (props: MiniToolbarProps) => { + const { t } = useLocalize() + const { showModal } = useUI() + + // show / hide for link input + const [showLinkInput, setShowLinkInput] = createSignal(false) + + // focus on link input when it shows up + createEffect(on(showLinkInput, (x?: boolean) => x && props.editor?.chain().focus().run())) + + const selection = createEditorTransaction( + () => props.editor, + (instance) => instance?.state.selection + ) + const [storedSelection, setStoredSelection] = createSignal() + const recoverSelection = () => { + if (!storedSelection()?.empty) { + createEditorTransaction( + () => props.editor, + (instance?: Editor) => { + const r = selection() + if (instance && r) { + instance.state.selection.from === r.from + instance.state.selection.to === r.to + } + } + ) + } + } + const storeSelection = () => { + const selection = props.editor?.state.selection + if (!selection?.empty) { + setStoredSelection(selection) + } + } + const toggleShowLink = () => { + if (showLinkInput()) { + props.editor?.chain().focus().run() + recoverSelection() + } else { + storeSelection() + } + setShowLinkInput(!showLinkInput()) + } + + return ( +
+ + {(instance) => ( +
+
+ 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')} + > + + +
+ + + +
+ )} +
+
+ ) +} diff --git a/src/components/Editor/EditorToolbar.tsx b/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx similarity index 93% rename from src/components/Editor/EditorToolbar.tsx rename to src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx index ec4c2f2b..1ea1d449 100644 --- a/src/components/Editor/EditorToolbar.tsx +++ b/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx @@ -4,13 +4,13 @@ import { createEditorTransaction, useEditorHTML, useEditorIsEmpty } from 'solid- import { useEditorContext } from '~/context/editor' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' -import { Button } from '../_shared/Button' -import { Icon } from '../_shared/Icon' -import { Loading } from '../_shared/Loading' -import { Popover } from '../_shared/Popover' -import { SimplifiedEditorProps } from './SimplifiedEditor' +import { Button } from '../../_shared/Button' +import { Icon } from '../../_shared/Icon' +import { Loading } from '../../_shared/Loading' +import { Popover } from '../../_shared/Popover' +import { SimplifiedEditorProps } from '../SimplifiedEditor' -import styles from './SimplifiedEditor.module.scss' +import styles from '../SimplifiedEditor.module.scss' export const ToolbarControls = ( props: SimplifiedEditorProps & { setShouldShowLinkBubbleMenu: (x: boolean) => void } diff --git a/src/components/Editor/EditorToolbar/ToolbarControl.tsx b/src/components/Editor/EditorToolbar/ToolbarControl.tsx new file mode 100644 index 00000000..b4046e67 --- /dev/null +++ b/src/components/Editor/EditorToolbar/ToolbarControl.tsx @@ -0,0 +1,40 @@ +import { Editor } from '@tiptap/core' +import clsx from 'clsx' +import { JSX } from 'solid-js' +import { Popover } from '~/components/_shared/Popover' + +import styles from '../SimplifiedEditor.module.scss' + +interface ControlProps { + editor: Editor + title: string + key: string + onChange: () => void + isActive?: (editor: Editor) => boolean + children: JSX.Element +} + +export const ToolbarControl = (props: ControlProps): JSX.Element => { + const handleClick = (ev?: MouseEvent) => { + ev?.preventDefault() + ev?.stopPropagation() + props.onChange?.() + } + + return ( + + {(triggerRef: (el: HTMLElement) => void) => ( + + )} + + ) +} + +export default ToolbarControl diff --git a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx index 825be6ff..9d7c479f 100644 --- a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx +++ b/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx @@ -3,7 +3,7 @@ import { createEffect, createSignal, onCleanup } from 'solid-js' import { useLocalize } from '~/context/localize' import { validateUrl } from '~/utils/validate' -import { InlineForm } from '../InlineForm' +import { InlineForm } from '../../_shared/InlineForm' type Props = { editor: Editor diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index 48233335..906f4193 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -1,68 +1,21 @@ -import type { Editor } from '@tiptap/core' import Placeholder from '@tiptap/extension-placeholder' import clsx from 'clsx' -import { type JSX, Show, createEffect, createReaction, createSignal, on, onCleanup } from 'solid-js' -import { - createEditorTransaction, - createTiptapEditor, - useEditorHTML, - useEditorIsEmpty, - useEditorIsFocused -} from 'solid-tiptap' -import { Icon } from '~/components/_shared/Icon/Icon' -import { Popover } from '~/components/_shared/Popover/Popover' -import { useLocalize } from '~/context/localize' +import { type JSX, createEffect, createSignal, on } from 'solid-js' +import { createTiptapEditor, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' import { minimal } from '~/lib/editorExtensions' -import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' +import { MicroToolbar } from '../EditorToolbar/MicroToolbar' 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 MicroEditorProps { content?: string onChange?: (content: string) => void + onSubmit?: (content: string) => void placeholder?: string } -const prevent = (e: Event) => e.preventDefault() - export const MicroEditor = (props: MicroEditorProps): JSX.Element => { - const { t } = useLocalize() const [editorElement, setEditorElement] = createSignal() - const [showLinkInput, setShowLinkInput] = createSignal(false) - const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) - const [toolbarElement, setToolbarElement] = createSignal() const editor = createTiptapEditor(() => ({ element: editorElement()!, @@ -78,36 +31,10 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { content: props.content || '' })) - const selection = createEditorTransaction(editor, (instance) => instance?.state.selection) - const [storedSelection, setStoredSelection] = createSignal() - const recoverSelection = () => { - if (!storedSelection()?.empty) { - // TODO set selection range from stored - createEditorTransaction(editor, (instance?: Editor) => { - const r = selection() - if (instance && r) { - instance.state.selection.from === r.from - instance.state.selection.to === r.to - } - }) - } - } - const storeSelection = (event: Event) => { - event.preventDefault() - const selection = editor()?.state.selection - if (!selection?.empty) { - setStoredSelection(selection) - } - } - const isEmpty = useEditorIsEmpty(editor) const isFocused = useEditorIsFocused(editor) const html = useEditorHTML(editor) - createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty))) createEffect(on(html, (c?: string) => c && props.onChange?.(c))) - createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run())) - createReaction(on(toolbarElement, (t?: HTMLElement) => t?.addEventListener('mousedown', prevent))) - onCleanup(() => toolbarElement()?.removeEventListener('mousedown', prevent)) return (
{ })} >
- - {(instance) => ( - -
-
-
- instance.chain().focus().toggleBold().run()} - title={t('Bold')} - > - - - instance.chain().focus().toggleItalic().run()} - title={t('Italic')} - > - - - setShowLinkInput(!showLinkInput())} - title={t('Add url')} - isActive={showLinkInput} - > - - -
- - { - setShowLinkInput(false) - recoverSelection() - }} - /> - -
-
-
- )} -
+ -
+
) diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index 4393f0b4..32f383b2 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -1,54 +1,18 @@ -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, on, onCleanup } from 'solid-js' -import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' -import { Toolbar } from 'terracotta' -import { Icon } from '~/components/_shared/Icon/Icon' -import { Popover } from '~/components/_shared/Popover/Popover' -import { useLocalize } from '~/context/localize' -import { useUI } from '~/context/ui' +import { type JSX, Show, createEffect, createSignal, on } from 'solid-js' +import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { base } from '~/lib/editorExtensions' -import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' +import { MiniToolbar } from '../EditorToolbar/MiniToolbar' 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 + onSubmit?: (content: string) => void + onCancel?: () => void limit?: number placeholder?: string } @@ -56,9 +20,6 @@ interface MiniEditorProps { export default function MiniEditor(props: MiniEditorProps): JSX.Element { const [editorElement, setEditorElement] = createSignal() const [counter, setCounter] = createSignal(0) - const [showLinkInput, setShowLinkInput] = createSignal(false) - const { t } = useLocalize() - const { showModal } = useUI() const editor = createTiptapEditor(() => ({ element: editorElement()!, @@ -77,7 +38,6 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { const html = useEditorHTML(editor) createEffect(on(html, (c?: string) => c && props.onChange?.(c))) - createEffect(on(showLinkInput, (x?: boolean) => x && editor()?.chain().focus().run())) createEffect(() => { const textLength = editor()?.getText().length || 0 @@ -86,85 +46,14 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { content && props.onChange?.(content) }) - const handleLinkClick = () => { - setShowLinkInput(!showLinkInput()) - editor()?.chain().focus().run() - } + const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused) - // 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}> diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index a474f9c7..f09c55c6 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -11,15 +11,15 @@ import { useUI } from '~/context/ui' import { base, custom } from '~/lib/editorExtensions' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' import { UploadedFile } from '~/types/upload' +import { UploadModalContent } from '../Upload/UploadModalContent' +import { renderUploadedImage } from '../Upload/renderUploadedImage' import { Modal } from '../_shared/Modal/Modal' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' -import { ToolbarControls } from './EditorToolbar' +import { ToolbarControls } from './EditorToolbar/SimplifiedToolbar' import { LinkBubbleMenuModule } from './LinkBubbleMenu' import { TextBubbleMenu } from './TextBubbleMenu' -import { UploadModalContent } from './UploadModalContent' -import { renderUploadedImage } from './renderUploadedImage' -import styles from './SimplifiedEditor.module.scss' +import styles from './Editor.module.scss' export type SimplifiedEditorProps = { placeholder: string diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx index 7d72ab0a..8352c69a 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx @@ -11,7 +11,7 @@ import { InsertLinkForm } from '../InsertLinkForm' import styles from './TextBubbleMenu.module.scss' -const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) +const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor')) type BubbleMenuProps = { editor: Editor @@ -146,18 +146,13 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => { - handleAddFootnote(value)} - variant={'bordered'} - initialContent={footNote()} + onSubmit={(value: string) => handleAddFootnote(value)} + content={footNote()} onCancel={() => { setFootnoteEditorOpen(false) }} - submitButtonText={t('Send')} /> diff --git a/src/components/Editor/index.ts b/src/components/Editor/index.ts index e2327288..4f9a58ab 100644 --- a/src/components/Editor/index.ts +++ b/src/components/Editor/index.ts @@ -1,4 +1,4 @@ export { EditorComponent as Editor } from './Editor' export { Panel } from './Panel' -export { TopicSelect } from './TopicSelect' -export { UploadModalContent } from './UploadModalContent' +export { TopicSelect } from '../TopicSelect' +export { UploadModalContent } from '../Upload/UploadModalContent' diff --git a/src/components/Editor/TopicSelect/TopicSelect.module.scss b/src/components/TopicSelect/TopicSelect.module.scss similarity index 100% rename from src/components/Editor/TopicSelect/TopicSelect.module.scss rename to src/components/TopicSelect/TopicSelect.module.scss diff --git a/src/components/Editor/TopicSelect/TopicSelect.tsx b/src/components/TopicSelect/TopicSelect.tsx similarity index 100% rename from src/components/Editor/TopicSelect/TopicSelect.tsx rename to src/components/TopicSelect/TopicSelect.tsx diff --git a/src/components/Editor/TopicSelect/index.ts b/src/components/TopicSelect/index.ts similarity index 100% rename from src/components/Editor/TopicSelect/index.ts rename to src/components/TopicSelect/index.ts diff --git a/src/components/Editor/AudioUploader/AudioUploader.module.scss b/src/components/Upload/AudioUploader/AudioUploader.module.scss similarity index 100% rename from src/components/Editor/AudioUploader/AudioUploader.module.scss rename to src/components/Upload/AudioUploader/AudioUploader.module.scss diff --git a/src/components/Editor/AudioUploader/AudioUploader.tsx b/src/components/Upload/AudioUploader/AudioUploader.tsx similarity index 100% rename from src/components/Editor/AudioUploader/AudioUploader.tsx rename to src/components/Upload/AudioUploader/AudioUploader.tsx diff --git a/src/components/Editor/AudioUploader/index.ts b/src/components/Upload/AudioUploader/index.ts similarity index 100% rename from src/components/Editor/AudioUploader/index.ts rename to src/components/Upload/AudioUploader/index.ts diff --git a/src/components/Editor/UploadModalContent/UploadModalContent.module.scss b/src/components/Upload/UploadModalContent/UploadModalContent.module.scss similarity index 100% rename from src/components/Editor/UploadModalContent/UploadModalContent.module.scss rename to src/components/Upload/UploadModalContent/UploadModalContent.module.scss diff --git a/src/components/Editor/UploadModalContent/UploadModalContent.tsx b/src/components/Upload/UploadModalContent/UploadModalContent.tsx similarity index 98% rename from src/components/Editor/UploadModalContent/UploadModalContent.tsx rename to src/components/Upload/UploadModalContent/UploadModalContent.tsx index 31ddc7fe..69b9f01f 100644 --- a/src/components/Editor/UploadModalContent/UploadModalContent.tsx +++ b/src/components/Upload/UploadModalContent/UploadModalContent.tsx @@ -10,7 +10,7 @@ import { useSession } from '~/context/session' import { useUI } from '~/context/ui' import { handleImageUpload } from '~/lib/handleImageUpload' import { UploadedFile } from '~/types/upload' -import { InlineForm } from '../InlineForm' +import { InlineForm } from '../../_shared/InlineForm' import styles from './UploadModalContent.module.scss' diff --git a/src/components/Editor/UploadModalContent/index.ts b/src/components/Upload/UploadModalContent/index.ts similarity index 100% rename from src/components/Editor/UploadModalContent/index.ts rename to src/components/Upload/UploadModalContent/index.ts diff --git a/src/components/Editor/VideoUploader/VideoUploader.module.scss b/src/components/Upload/VideoUploader/VideoUploader.module.scss similarity index 100% rename from src/components/Editor/VideoUploader/VideoUploader.module.scss rename to src/components/Upload/VideoUploader/VideoUploader.module.scss diff --git a/src/components/Editor/VideoUploader/VideoUploader.tsx b/src/components/Upload/VideoUploader/VideoUploader.tsx similarity index 100% rename from src/components/Editor/VideoUploader/VideoUploader.tsx rename to src/components/Upload/VideoUploader/VideoUploader.tsx diff --git a/src/components/Editor/VideoUploader/index.ts b/src/components/Upload/VideoUploader/index.ts similarity index 100% rename from src/components/Editor/VideoUploader/index.ts rename to src/components/Upload/VideoUploader/index.ts diff --git a/src/components/Editor/renderUploadedImage.ts b/src/components/Upload/renderUploadedImage.ts similarity index 100% rename from src/components/Editor/renderUploadedImage.ts rename to src/components/Upload/renderUploadedImage.ts diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 0e22f777..25dd9d7c 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -21,14 +21,14 @@ import { LayoutType } from '~/types/common' import { MediaItem } from '~/types/mediaitem' import { clone } from '~/utils/clone' import { Editor as EditorComponent, Panel } from '../../Editor' -import { AudioUploader } from '../../Editor/AudioUploader' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' -import { VideoUploader } from '../../Editor/VideoUploader' +import { AudioUploader } from '../../Upload/AudioUploader' +import { VideoUploader } from '../../Upload/VideoUploader' import { Modal } from '../../_shared/Modal' import { TableOfContents } from '../../_shared/TableOfContents' import styles from './EditView.module.scss' -const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) +const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor')) const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea')) type Props = { @@ -358,13 +358,10 @@ export const EditView = (props: Props) => { /> - handleInputChange('lead', value)} + content={form.lead} + onChange={(value: string) => handleInputChange('lead', value)} /> diff --git a/src/components/Views/Inbox/Inbox.tsx b/src/components/Views/Inbox/Inbox.tsx index 87dd02eb..ffc4e7c8 100644 --- a/src/components/Views/Inbox/Inbox.tsx +++ b/src/components/Views/Inbox/Inbox.tsx @@ -1,6 +1,6 @@ import { useNavigate } from '@solidjs/router' import { clsx } from 'clsx' -import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' +import { For, Show, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js' import QuotedMessage from '~/components/Inbox/QuotedMessage' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' @@ -17,7 +17,6 @@ import type { } from '~/graphql/schema/chat.gen' import type { Author } from '~/graphql/schema/core.gen' import { getShortDate } from '~/utils/date' -import SimplifiedEditor from '../../Editor/SimplifiedEditor' import DialogCard from '../../Inbox/DialogCard' import DialogHeader from '../../Inbox/DialogHeader' import { Message } from '../../Inbox/Message' @@ -26,6 +25,8 @@ import Search from '../../Inbox/Search' import { Modal } from '../../_shared/Modal' import styles from './Inbox.module.scss' +const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor')) + const userSearch = (array: Author[], keyword: string) => { return array.filter((value) => new RegExp(keyword.trim(), 'gi').test(value.name || '')) } @@ -38,7 +39,6 @@ export const InboxView = (props: { authors: Author[]; chat?: Chat }) => { const [sortByPerToPer, setSortByPerToPer] = createSignal(false) const [currentDialog, setCurrentDialog] = createSignal() const [messageToReply, setMessageToReply] = createSignal(null) - const [isClear, setClear] = createSignal(false) const [isScrollToNewVisible, setIsScrollToNewVisible] = createSignal(false) const { session } = useSession() const authorId = createMemo(() => session()?.user?.app_data?.profile?.id || 0) @@ -77,11 +77,9 @@ export const InboxView = (props: { authors: Author[]; chat?: Chat }) => { reply_to: messageToReply()?.id, chat_id: currentDialog()?.id || '' } as MutationCreate_MessageArgs) - setClear(true) setMessageToReply(null) if (messagesContainerRef) (messagesContainerRef as HTMLDivElement).scrollTop = messagesContainerRef?.scrollHeight || 0 - setClear(false) } createEffect( @@ -291,15 +289,7 @@ export const InboxView = (props: { authors: Author[]; chat?: Chat }) => { />
- handleSubmit(message)} - submitByCtrlEnter={true} - /> +
diff --git a/src/components/Views/Profile/ProfileSettings.tsx b/src/components/Views/Profile/ProfileSettings.tsx index daa0e22a..7cfab5d9 100644 --- a/src/components/Views/Profile/ProfileSettings.tsx +++ b/src/components/Views/Profile/ProfileSettings.tsx @@ -14,7 +14,6 @@ import { onMount } from 'solid-js' import { createStore } from 'solid-js/store' -import SimplifiedEditor from '~/components/Editor/SimplifiedEditor' import { useLocalize } from '~/context/localize' import { useProfile } from '~/context/profile' import { useSession } from '~/context/session' @@ -35,7 +34,7 @@ import { SocialNetworkInput } from '../../_shared/SocialNetworkInput' import styles from './Settings.module.scss' import { profileSocialLinks } from './profileSocialLinks' -// const SimplifiedEditor = lazy(() => import('~/components/Editor/SimplifiedEditor')) +const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor')) const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea')) function filterNulls(arr: InputMaybe[]): string[] { @@ -340,18 +339,7 @@ export const ProfileSettings = () => { />

{t('About')}

- +

{t('Social networks')}

diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 77db6d1b..9c848a52 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -18,7 +18,7 @@ import { Modal } from '../../_shared/Modal' import stylesBeside from '../../Feed/Beside.module.scss' import styles from './PublishSettings.module.scss' -const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) +const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor')) const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/GrowingTextarea')) const DESCRIPTION_MAX_LENGTH = 400 @@ -224,16 +224,10 @@ export const PublishSettings = (props: Props) => { allowEnterKey={false} maxLength={100} /> - - onChange={(value: any) => setForm('description', value)} - maxLength={DESCRIPTION_MAX_LENGTH} + content={composeDescription()} + onChange={(value?: string) => value && setForm('description', value)} />
diff --git a/src/components/Editor/InlineForm/InlineForm.module.scss b/src/components/_shared/InlineForm/InlineForm.module.scss similarity index 100% rename from src/components/Editor/InlineForm/InlineForm.module.scss rename to src/components/_shared/InlineForm/InlineForm.module.scss diff --git a/src/components/Editor/InlineForm/InlineForm.tsx b/src/components/_shared/InlineForm/InlineForm.tsx similarity index 100% rename from src/components/Editor/InlineForm/InlineForm.tsx rename to src/components/_shared/InlineForm/InlineForm.tsx diff --git a/src/components/Editor/InlineForm/index.ts b/src/components/_shared/InlineForm/index.ts similarity index 100% rename from src/components/Editor/InlineForm/index.ts rename to src/components/_shared/InlineForm/index.ts diff --git a/src/components/_shared/SolidSwiper/EditorSwiper.tsx b/src/components/_shared/SolidSwiper/EditorSwiper.tsx index c65af966..303086d7 100644 --- a/src/components/_shared/SolidSwiper/EditorSwiper.tsx +++ b/src/components/_shared/SolidSwiper/EditorSwiper.tsx @@ -3,7 +3,6 @@ import { clsx } from 'clsx' import { For, Show, createEffect, createSignal, lazy, on, onMount } from 'solid-js' import SwiperCore from 'swiper' import { Manipulation, Navigation, Pagination } from 'swiper/modules' - import { useLocalize } from '~/context/localize' import { useSnackbar } from '~/context/ui' import { composeMediaItems } from '~/lib/composeMediaItems' @@ -23,7 +22,7 @@ import { MediaItem } from '~/types/mediaitem' import { UploadedFile } from '~/types/upload' import styles from './Swiper.module.scss' -const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) +const MicroEditor = lazy(() => import('../../Editor/MicroEditor/MicroEditor')) type Props = { images: MediaItem[] @@ -316,9 +315,8 @@ export const EditorSwiper = (props: Props) => { value={props.images[slideIndex()]?.source} onChange={(event) => handleSlideDescriptionChange(slideIndex(), 'source', event.target.value)} /> - setSlideBody(value)} /> diff --git a/src/entry-client.tsx b/src/entry-client.tsx index a7df337b..540bffc0 100644 --- a/src/entry-client.tsx +++ b/src/entry-client.tsx @@ -9,3 +9,5 @@ mount(() => , document.getElementById('app') || document.body) // navigator.serviceWorker.register(`/sw.js`); // }); // } + +export default {} From 76dea4341dbd4782115238791c0f42533299eeea Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 20:21:52 +0300 Subject: [PATCH 18/23] toolbar-appear-fix --- src/components/Article/CommentsTree.tsx | 2 +- .../Editor/EditorToolbar/MicroToolbar.tsx | 16 ++++++++-------- .../Editor/EditorToolbar/MiniToolbar.tsx | 18 +++++++++--------- .../Editor/MicroEditor/MicroEditor.tsx | 2 +- .../Editor/MiniEditor/MiniEditor.tsx | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index ca3b9e43..09b42e77 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -156,7 +156,7 @@ export const CommentsTree = (props: Props) => { > handleSubmitComment(value)} + onSubmit={handleSubmitComment} /> diff --git a/src/components/Editor/EditorToolbar/MicroToolbar.tsx b/src/components/Editor/EditorToolbar/MicroToolbar.tsx index 8e9b42db..10c88404 100644 --- a/src/components/Editor/EditorToolbar/MicroToolbar.tsx +++ b/src/components/Editor/EditorToolbar/MicroToolbar.tsx @@ -1,5 +1,5 @@ import { Editor } from '@tiptap/core' -import { Show, createEffect, createSignal, on } from 'solid-js' +import { Accessor, Show, createEffect, createSignal, on } from 'solid-js' import { createEditorTransaction } from 'solid-tiptap' import { Icon } from '~/components/_shared/Icon/Icon' import { useLocalize } from '~/context/localize' @@ -10,7 +10,7 @@ import styles from '../SimplifiedEditor.module.scss' export interface MicroToolbarProps { showing?: boolean - editor?: Editor + editor: Accessor } export const MicroToolbar = (props: MicroToolbarProps) => { @@ -19,7 +19,7 @@ export const MicroToolbar = (props: MicroToolbarProps) => { // show / hide for menu const [showSimpleMenu, setShowSimpleMenu] = createSignal(!props.showing) const selection = createEditorTransaction( - () => props.editor, + props.editor, (instance) => instance?.state.selection ) @@ -30,13 +30,13 @@ export const MicroToolbar = (props: MicroToolbarProps) => { createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty))) // focus on link input when it shows up - createEffect(on(showLinkInput, (x?: boolean) => x && props.editor?.chain().focus().run())) + createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run())) const [storedSelection, setStoredSelection] = createSignal() const recoverSelection = () => { if (!storedSelection()?.empty) { createEditorTransaction( - () => props.editor, + props.editor, (instance?: Editor) => { const r = selection() if (instance && r) { @@ -48,14 +48,14 @@ export const MicroToolbar = (props: MicroToolbarProps) => { } } const storeSelection = () => { - const selection = props.editor?.state.selection + const selection = props.editor()?.state.selection if (!selection?.empty) { setStoredSelection(selection) } } const toggleShowLink = () => { if (showLinkInput()) { - props.editor?.chain().focus().run() + props.editor()?.chain().focus().run() recoverSelection() } else { storeSelection() @@ -63,7 +63,7 @@ export const MicroToolbar = (props: MicroToolbarProps) => { setShowLinkInput(!showLinkInput()) } return ( - + {(instance) => (
} export const MiniToolbar = (props: MiniToolbarProps) => { @@ -21,17 +21,17 @@ export const MiniToolbar = (props: MiniToolbarProps) => { const [showLinkInput, setShowLinkInput] = createSignal(false) // focus on link input when it shows up - createEffect(on(showLinkInput, (x?: boolean) => x && props.editor?.chain().focus().run())) + createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run())) const selection = createEditorTransaction( - () => props.editor, + props.editor, (instance) => instance?.state.selection ) const [storedSelection, setStoredSelection] = createSignal() const recoverSelection = () => { if (!storedSelection()?.empty) { createEditorTransaction( - () => props.editor, + props.editor, (instance?: Editor) => { const r = selection() if (instance && r) { @@ -43,14 +43,14 @@ export const MiniToolbar = (props: MiniToolbarProps) => { } } const storeSelection = () => { - const selection = props.editor?.state.selection + const selection = props.editor()?.state.selection if (!selection?.empty) { setStoredSelection(selection) } } const toggleShowLink = () => { if (showLinkInput()) { - props.editor?.chain().focus().run() + props.editor()?.chain().focus().run() recoverSelection() } else { storeSelection() @@ -60,7 +60,7 @@ export const MiniToolbar = (props: MiniToolbarProps) => { return (
- + {(instance) => (
diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index 906f4193..42cbe8b8 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -43,7 +43,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { })} >
- +
diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index 32f383b2..ac63ab2f 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -53,7 +53,7 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
- + 0}> From 258b579d05adfa838c42aea8df6dffae85ca79a5 Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 20:57:25 +0300 Subject: [PATCH 19/23] editor-toolbar --- src/components/Article/CommentsTree.tsx | 5 +- .../{MiniToolbar.tsx => EditorToolbar.tsx} | 83 ++++++++----- .../Editor/EditorToolbar/MicroToolbar.tsx | 113 ------------------ .../Editor/MicroEditor/MicroEditor.tsx | 14 +-- .../Editor/MiniEditor/MiniEditor.tsx | 25 +++- 5 files changed, 79 insertions(+), 161 deletions(-) rename src/components/Editor/EditorToolbar/{MiniToolbar.tsx => EditorToolbar.tsx} (55%) delete mode 100644 src/components/Editor/EditorToolbar/MicroToolbar.tsx diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 09b42e77..4d666b5b 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -154,10 +154,7 @@ export const CommentsTree = (props: Props) => {
} > - + diff --git a/src/components/Editor/EditorToolbar/MiniToolbar.tsx b/src/components/Editor/EditorToolbar/EditorToolbar.tsx similarity index 55% rename from src/components/Editor/EditorToolbar/MiniToolbar.tsx rename to src/components/Editor/EditorToolbar/EditorToolbar.tsx index 21446da3..49e63729 100644 --- a/src/components/Editor/EditorToolbar/MiniToolbar.tsx +++ b/src/components/Editor/EditorToolbar/EditorToolbar.tsx @@ -1,19 +1,25 @@ import { Editor } from '@tiptap/core' import { Accessor, Show, createEffect, createSignal, on } from 'solid-js' +import { Portal } from 'solid-js/web' import { createEditorTransaction } from 'solid-tiptap' +import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent' +import { renderUploadedImage } from '~/components/Upload/renderUploadedImage' import { Icon } from '~/components/_shared/Icon/Icon' +import { Modal } from '~/components/_shared/Modal/Modal' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' +import { UploadedFile } from '~/types/upload' import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' import { ToolbarControl as Control } from './ToolbarControl' import styles from '../SimplifiedEditor.module.scss' -interface MiniToolbarProps { +interface EditorToolbarProps { editor: Accessor + mode?: 'micro' | 'mini' } -export const MiniToolbar = (props: MiniToolbarProps) => { +export const EditorToolbar = (props: EditorToolbarProps) => { const { t } = useLocalize() const { showModal } = useUI() @@ -23,23 +29,24 @@ export const MiniToolbar = (props: MiniToolbarProps) => { // focus on link input when it shows up createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run())) - const selection = createEditorTransaction( - props.editor, - (instance) => instance?.state.selection + const selection = createEditorTransaction(props.editor, (instance) => instance?.state.selection) + + // change visibility on selection if not in link input mode + const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) + createEffect( + on([selection, showLinkInput], ([s, l]) => props.mode === 'micro' && !l && setShowSimpleMenu(!s?.empty)) ) + const [storedSelection, setStoredSelection] = createSignal() const recoverSelection = () => { if (!storedSelection()?.empty) { - createEditorTransaction( - props.editor, - (instance?: Editor) => { - const r = selection() - if (instance && r) { - instance.state.selection.from === r.from - instance.state.selection.to === r.to - } + createEditorTransaction(props.editor, (instance?: Editor) => { + const r = selection() + if (instance && r) { + instance.state.selection.from === r.from + instance.state.selection.to === r.to } - ) + }) } } const storeSelection = () => { @@ -60,7 +67,10 @@ export const MiniToolbar = (props: MiniToolbarProps) => { return (
- + {(instance) => (
@@ -89,26 +99,37 @@ export const MiniToolbar = (props: MiniToolbarProps) => { > - instance.chain().focus().toggleBlockquote().run()} - title={t('Add blockquote')} - > - - - showModal('simplifiedEditorUploadImage')} - title={t('Add image')} - > - - + + instance.chain().focus().toggleBlockquote().run()} + title={t('Add blockquote')} + > + + + showModal('simplifiedEditorUploadImage')} + title={t('Add image')} + > + + +
+ + + + + renderUploadedImage(instance as Editor, image as UploadedFile)} + /> + +
)}
diff --git a/src/components/Editor/EditorToolbar/MicroToolbar.tsx b/src/components/Editor/EditorToolbar/MicroToolbar.tsx deleted file mode 100644 index 10c88404..00000000 --- a/src/components/Editor/EditorToolbar/MicroToolbar.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Editor } from '@tiptap/core' -import { Accessor, Show, createEffect, createSignal, on } from 'solid-js' -import { createEditorTransaction } from 'solid-tiptap' -import { Icon } from '~/components/_shared/Icon/Icon' -import { useLocalize } from '~/context/localize' -import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' -import { ToolbarControl as Control } from './ToolbarControl' - -import styles from '../SimplifiedEditor.module.scss' - -export interface MicroToolbarProps { - showing?: boolean - editor: Accessor -} - -export const MicroToolbar = (props: MicroToolbarProps) => { - const { t } = useLocalize() - - // show / hide for menu - const [showSimpleMenu, setShowSimpleMenu] = createSignal(!props.showing) - const selection = createEditorTransaction( - props.editor, - (instance) => instance?.state.selection - ) - - // show / hide for link input - const [showLinkInput, setShowLinkInput] = createSignal(false) - - // change visibility on selection if not in link input mode - createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty))) - - // focus on link input when it shows up - createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run())) - - const [storedSelection, setStoredSelection] = createSignal() - const recoverSelection = () => { - if (!storedSelection()?.empty) { - createEditorTransaction( - props.editor, - (instance?: Editor) => { - const r = selection() - if (instance && r) { - instance.state.selection.from === r.from - instance.state.selection.to === r.to - } - } - ) - } - } - const storeSelection = () => { - const selection = props.editor()?.state.selection - if (!selection?.empty) { - setStoredSelection(selection) - } - } - const toggleShowLink = () => { - if (showLinkInput()) { - props.editor()?.chain().focus().run() - recoverSelection() - } else { - storeSelection() - } - setShowLinkInput(!showLinkInput()) - } - return ( - - {(instance) => ( - -
-
-
- instance.chain().focus().toggleBold().run()} - title={t('Bold')} - > - - - instance.chain().focus().toggleItalic().run()} - title={t('Italic')} - > - - - - - -
- - - -
-
-
- )} -
- ) -} diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index 42cbe8b8..549dd54a 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -1,9 +1,9 @@ import Placeholder from '@tiptap/extension-placeholder' import clsx from 'clsx' import { type JSX, createEffect, createSignal, on } from 'solid-js' -import { createTiptapEditor, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' +import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tiptap' import { minimal } from '~/lib/editorExtensions' -import { MicroToolbar } from '../EditorToolbar/MicroToolbar' +import { EditorToolbar } from '../EditorToolbar/EditorToolbar' import styles from '../SimplifiedEditor.module.scss' @@ -31,20 +31,14 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { content: props.content || '' })) - const isEmpty = useEditorIsEmpty(editor) const isFocused = useEditorIsFocused(editor) const html = useEditorHTML(editor) createEffect(on(html, (c?: string) => c && props.onChange?.(c))) return ( -
+
- - +
diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index ac63ab2f..1b86aa82 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -4,8 +4,10 @@ import clsx from 'clsx' import { type JSX, Show, createEffect, createSignal, on } from 'solid-js' import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { base } from '~/lib/editorExtensions' +import { EditorToolbar } from '../EditorToolbar/EditorToolbar' -import { MiniToolbar } from '../EditorToolbar/MiniToolbar' +import { Button } from '~/components/_shared/Button' +import { useLocalize } from '~/context/localize' import styles from '../SimplifiedEditor.module.scss' interface MiniEditorProps { @@ -18,6 +20,7 @@ interface MiniEditorProps { } export default function MiniEditor(props: MiniEditorProps): JSX.Element { + const { t } = useLocalize() const [editorElement, setEditorElement] = createSignal() const [counter, setCounter] = createSignal(0) @@ -36,7 +39,10 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { content: props.content || '' })) + const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused) + const isEmpty = createEditorTransaction(editor, (instance) => instance?.isEmpty) const html = useEditorHTML(editor) + createEffect(on(html, (c?: string) => c && props.onChange?.(c))) createEffect(() => { @@ -46,14 +52,27 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { content && props.onChange?.(content) }) - const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused) + const handleSubmit = () => { + html() && props.onSubmit?.(html() || '') + editor()?.commands.clearContent(true) + } return (
- + + +
+
0}> From 90cd3988a12b5785ae94cb9a75f0549ba0ce9d3e Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 20:57:25 +0300 Subject: [PATCH 20/23] editor-toolbar --- src/components/Article/CommentsTree.tsx | 5 +- .../{MiniToolbar.tsx => EditorToolbar.tsx} | 85 ++++++++----- .../InsertLinkForm.tsx | 0 .../Editor/EditorToolbar/MicroToolbar.tsx | 113 ------------------ src/components/Editor/InsertLinkForm/index.ts | 1 - .../Editor/MicroEditor/MicroEditor.tsx | 14 +-- .../Editor/MiniEditor/MiniEditor.tsx | 25 +++- .../Editor/TextBubbleMenu/TextBubbleMenu.tsx | 4 +- 8 files changed, 81 insertions(+), 166 deletions(-) rename src/components/Editor/EditorToolbar/{MiniToolbar.tsx => EditorToolbar.tsx} (53%) rename src/components/Editor/{InsertLinkForm => EditorToolbar}/InsertLinkForm.tsx (100%) delete mode 100644 src/components/Editor/EditorToolbar/MicroToolbar.tsx delete mode 100644 src/components/Editor/InsertLinkForm/index.ts diff --git a/src/components/Article/CommentsTree.tsx b/src/components/Article/CommentsTree.tsx index 09b42e77..4d666b5b 100644 --- a/src/components/Article/CommentsTree.tsx +++ b/src/components/Article/CommentsTree.tsx @@ -154,10 +154,7 @@ export const CommentsTree = (props: Props) => {
} > - + diff --git a/src/components/Editor/EditorToolbar/MiniToolbar.tsx b/src/components/Editor/EditorToolbar/EditorToolbar.tsx similarity index 53% rename from src/components/Editor/EditorToolbar/MiniToolbar.tsx rename to src/components/Editor/EditorToolbar/EditorToolbar.tsx index 21446da3..06cef005 100644 --- a/src/components/Editor/EditorToolbar/MiniToolbar.tsx +++ b/src/components/Editor/EditorToolbar/EditorToolbar.tsx @@ -1,19 +1,25 @@ import { Editor } from '@tiptap/core' import { Accessor, Show, createEffect, createSignal, on } from 'solid-js' +import { Portal } from 'solid-js/web' import { createEditorTransaction } from 'solid-tiptap' +import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent' +import { renderUploadedImage } from '~/components/Upload/renderUploadedImage' import { Icon } from '~/components/_shared/Icon/Icon' +import { Modal } from '~/components/_shared/Modal/Modal' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' -import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' +import { UploadedFile } from '~/types/upload' +import { InsertLinkForm } from './InsertLinkForm' import { ToolbarControl as Control } from './ToolbarControl' import styles from '../SimplifiedEditor.module.scss' -interface MiniToolbarProps { +interface EditorToolbarProps { editor: Accessor + mode?: 'micro' | 'mini' } -export const MiniToolbar = (props: MiniToolbarProps) => { +export const EditorToolbar = (props: EditorToolbarProps) => { const { t } = useLocalize() const { showModal } = useUI() @@ -23,23 +29,24 @@ export const MiniToolbar = (props: MiniToolbarProps) => { // focus on link input when it shows up createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run())) - const selection = createEditorTransaction( - props.editor, - (instance) => instance?.state.selection + const selection = createEditorTransaction(props.editor, (instance) => instance?.state.selection) + + // change visibility on selection if not in link input mode + const [showSimpleMenu, setShowSimpleMenu] = createSignal(false) + createEffect( + on([selection, showLinkInput], ([s, l]) => props.mode === 'micro' && !l && setShowSimpleMenu(!s?.empty)) ) + const [storedSelection, setStoredSelection] = createSignal() const recoverSelection = () => { if (!storedSelection()?.empty) { - createEditorTransaction( - props.editor, - (instance?: Editor) => { - const r = selection() - if (instance && r) { - instance.state.selection.from === r.from - instance.state.selection.to === r.to - } + createEditorTransaction(props.editor, (instance?: Editor) => { + const r = selection() + if (instance && r) { + instance.state.selection.from === r.from + instance.state.selection.to === r.to } - ) + }) } } const storeSelection = () => { @@ -60,7 +67,10 @@ export const MiniToolbar = (props: MiniToolbarProps) => { return (
- + {(instance) => (
@@ -89,26 +99,37 @@ export const MiniToolbar = (props: MiniToolbarProps) => { > - instance.chain().focus().toggleBlockquote().run()} - title={t('Add blockquote')} - > - - - showModal('simplifiedEditorUploadImage')} - title={t('Add image')} - > - - + + instance.chain().focus().toggleBlockquote().run()} + title={t('Add blockquote')} + > + + + showModal('simplifiedEditorUploadImage')} + title={t('Add image')} + > + + +
+ + + + + renderUploadedImage(instance as Editor, image as UploadedFile)} + /> + +
)}
diff --git a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx b/src/components/Editor/EditorToolbar/InsertLinkForm.tsx similarity index 100% rename from src/components/Editor/InsertLinkForm/InsertLinkForm.tsx rename to src/components/Editor/EditorToolbar/InsertLinkForm.tsx diff --git a/src/components/Editor/EditorToolbar/MicroToolbar.tsx b/src/components/Editor/EditorToolbar/MicroToolbar.tsx deleted file mode 100644 index 10c88404..00000000 --- a/src/components/Editor/EditorToolbar/MicroToolbar.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import { Editor } from '@tiptap/core' -import { Accessor, Show, createEffect, createSignal, on } from 'solid-js' -import { createEditorTransaction } from 'solid-tiptap' -import { Icon } from '~/components/_shared/Icon/Icon' -import { useLocalize } from '~/context/localize' -import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' -import { ToolbarControl as Control } from './ToolbarControl' - -import styles from '../SimplifiedEditor.module.scss' - -export interface MicroToolbarProps { - showing?: boolean - editor: Accessor -} - -export const MicroToolbar = (props: MicroToolbarProps) => { - const { t } = useLocalize() - - // show / hide for menu - const [showSimpleMenu, setShowSimpleMenu] = createSignal(!props.showing) - const selection = createEditorTransaction( - props.editor, - (instance) => instance?.state.selection - ) - - // show / hide for link input - const [showLinkInput, setShowLinkInput] = createSignal(false) - - // change visibility on selection if not in link input mode - createEffect(on([selection, showLinkInput], ([s, l]) => !l && setShowSimpleMenu(!s?.empty))) - - // focus on link input when it shows up - createEffect(on(showLinkInput, (x?: boolean) => x && props.editor()?.chain().focus().run())) - - const [storedSelection, setStoredSelection] = createSignal() - const recoverSelection = () => { - if (!storedSelection()?.empty) { - createEditorTransaction( - props.editor, - (instance?: Editor) => { - const r = selection() - if (instance && r) { - instance.state.selection.from === r.from - instance.state.selection.to === r.to - } - } - ) - } - } - const storeSelection = () => { - const selection = props.editor()?.state.selection - if (!selection?.empty) { - setStoredSelection(selection) - } - } - const toggleShowLink = () => { - if (showLinkInput()) { - props.editor()?.chain().focus().run() - recoverSelection() - } else { - storeSelection() - } - setShowLinkInput(!showLinkInput()) - } - return ( - - {(instance) => ( - -
-
-
- instance.chain().focus().toggleBold().run()} - title={t('Bold')} - > - - - instance.chain().focus().toggleItalic().run()} - title={t('Italic')} - > - - - - - -
- - - -
-
-
- )} -
- ) -} diff --git a/src/components/Editor/InsertLinkForm/index.ts b/src/components/Editor/InsertLinkForm/index.ts deleted file mode 100644 index 4cf74dea..00000000 --- a/src/components/Editor/InsertLinkForm/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { InsertLinkForm } from './InsertLinkForm' diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index 42cbe8b8..549dd54a 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -1,9 +1,9 @@ import Placeholder from '@tiptap/extension-placeholder' import clsx from 'clsx' import { type JSX, createEffect, createSignal, on } from 'solid-js' -import { createTiptapEditor, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' +import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tiptap' import { minimal } from '~/lib/editorExtensions' -import { MicroToolbar } from '../EditorToolbar/MicroToolbar' +import { EditorToolbar } from '../EditorToolbar/EditorToolbar' import styles from '../SimplifiedEditor.module.scss' @@ -31,20 +31,14 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { content: props.content || '' })) - const isEmpty = useEditorIsEmpty(editor) const isFocused = useEditorIsFocused(editor) const html = useEditorHTML(editor) createEffect(on(html, (c?: string) => c && props.onChange?.(c))) return ( -
+
- - +
diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index ac63ab2f..1b86aa82 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -4,8 +4,10 @@ import clsx from 'clsx' import { type JSX, Show, createEffect, createSignal, on } from 'solid-js' import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { base } from '~/lib/editorExtensions' +import { EditorToolbar } from '../EditorToolbar/EditorToolbar' -import { MiniToolbar } from '../EditorToolbar/MiniToolbar' +import { Button } from '~/components/_shared/Button' +import { useLocalize } from '~/context/localize' import styles from '../SimplifiedEditor.module.scss' interface MiniEditorProps { @@ -18,6 +20,7 @@ interface MiniEditorProps { } export default function MiniEditor(props: MiniEditorProps): JSX.Element { + const { t } = useLocalize() const [editorElement, setEditorElement] = createSignal() const [counter, setCounter] = createSignal(0) @@ -36,7 +39,10 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { content: props.content || '' })) + const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused) + const isEmpty = createEditorTransaction(editor, (instance) => instance?.isEmpty) const html = useEditorHTML(editor) + createEffect(on(html, (c?: string) => c && props.onChange?.(c))) createEffect(() => { @@ -46,14 +52,27 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { content && props.onChange?.(content) }) - const isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused) + const handleSubmit = () => { + html() && props.onSubmit?.(html() || '') + editor()?.commands.clearContent(true) + } return (
- + + +
+
0}> diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx index 8352c69a..981523dc 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx @@ -1,13 +1,11 @@ import type { Editor } from '@tiptap/core' - import { clsx } from 'clsx' import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { createEditorTransaction } from 'solid-tiptap' - import { Icon } from '~/components/_shared/Icon' import { Popover } from '~/components/_shared/Popover' import { useLocalize } from '~/context/localize' -import { InsertLinkForm } from '../InsertLinkForm' +import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm' import styles from './TextBubbleMenu.module.scss' From 30de1ddb3e44e575405d49a148657ad8b4ce76ff Mon Sep 17 00:00:00 2001 From: Untone Date: Fri, 27 Sep 2024 21:09:50 +0300 Subject: [PATCH 21/23] editor-refactored-2 --- .../TextBubbleMenu.module.scss | 0 .../TextBubbleMenu.tsx | 4 +- .../{Prosemirror.scss => Editor.module.scss} | 0 src/components/Editor/Editor.tsx | 4 +- .../Editor/EditorToolbar/EditorToolbar.tsx | 2 +- .../Editor/EditorToolbar/InsertLinkForm.tsx | 1 - .../EditorToolbar/SimplifiedToolbar.tsx | 133 ---------- .../Editor/EditorToolbar/ToolbarControl.tsx | 2 +- .../LinkBubbleMenu/LinkBubbleMenu.module.scss | 4 - .../LinkBubbleMenu/LinkBubbleMenu.module.tsx | 19 -- src/components/Editor/LinkBubbleMenu/index.ts | 1 - .../Editor/MicroEditor/MicroEditor.tsx | 4 +- .../MiniEditor.module.scss} | 2 +- .../Editor/MiniEditor/MiniEditor.tsx | 8 +- src/components/Editor/SimplifiedEditor.tsx | 244 ------------------ src/components/Editor/TextBubbleMenu/index.ts | 1 - src/components/Editor/index.ts | 4 - .../Views/EditView/EditSettingsView.tsx | 2 +- src/components/Views/EditView/EditView.tsx | 3 +- .../Views/PublishSettings/PublishSettings.tsx | 3 +- 20 files changed, 18 insertions(+), 423 deletions(-) rename src/components/Editor/{TextBubbleMenu => BubbleMenu}/TextBubbleMenu.module.scss (100%) rename src/components/Editor/{TextBubbleMenu => BubbleMenu}/TextBubbleMenu.tsx (99%) rename src/components/Editor/{Prosemirror.scss => Editor.module.scss} (100%) delete mode 100644 src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx delete mode 100644 src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss delete mode 100644 src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx delete mode 100644 src/components/Editor/LinkBubbleMenu/index.ts rename src/components/Editor/{SimplifiedEditor.module.scss => MiniEditor/MiniEditor.module.scss} (99%) delete mode 100644 src/components/Editor/SimplifiedEditor.tsx delete mode 100644 src/components/Editor/TextBubbleMenu/index.ts delete mode 100644 src/components/Editor/index.ts diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss b/src/components/Editor/BubbleMenu/TextBubbleMenu.module.scss similarity index 100% rename from src/components/Editor/TextBubbleMenu/TextBubbleMenu.module.scss rename to src/components/Editor/BubbleMenu/TextBubbleMenu.module.scss diff --git a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx similarity index 99% rename from src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx rename to src/components/Editor/BubbleMenu/TextBubbleMenu.tsx index 981523dc..73d86c0b 100644 --- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx @@ -7,9 +7,9 @@ import { Popover } from '~/components/_shared/Popover' import { useLocalize } from '~/context/localize' import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm' -import styles from './TextBubbleMenu.module.scss' +import styles from '../TextBubbleMenu/TextBubbleMenu.module.scss' -const MiniEditor = lazy(() => import('../../Editor/MiniEditor/MiniEditor')) +const MiniEditor = lazy(() => import('../MiniEditor/MiniEditor')) type BubbleMenuProps = { editor: Editor diff --git a/src/components/Editor/Prosemirror.scss b/src/components/Editor/Editor.module.scss similarity index 100% rename from src/components/Editor/Prosemirror.scss rename to src/components/Editor/Editor.module.scss diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 4a05a420..b49b2799 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -18,10 +18,10 @@ import { base, custom, extended } from '~/lib/editorExtensions' import { handleImageUpload } from '~/lib/handleImageUpload' import { renderUploadedImage } from '../Upload/renderUploadedImage' import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu' +import { TextBubbleMenu } from './BubbleMenu/TextBubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' -import { TextBubbleMenu } from './TextBubbleMenu' -import './Prosemirror.scss' +import './Editor.module.scss' export type EditorComponentProps = { shoutId: number diff --git a/src/components/Editor/EditorToolbar/EditorToolbar.tsx b/src/components/Editor/EditorToolbar/EditorToolbar.tsx index 06cef005..3632bc29 100644 --- a/src/components/Editor/EditorToolbar/EditorToolbar.tsx +++ b/src/components/Editor/EditorToolbar/EditorToolbar.tsx @@ -12,7 +12,7 @@ import { UploadedFile } from '~/types/upload' import { InsertLinkForm } from './InsertLinkForm' import { ToolbarControl as Control } from './ToolbarControl' -import styles from '../SimplifiedEditor.module.scss' +import styles from '../MiniEditor/MiniEditor.module.scss' interface EditorToolbarProps { editor: Accessor diff --git a/src/components/Editor/EditorToolbar/InsertLinkForm.tsx b/src/components/Editor/EditorToolbar/InsertLinkForm.tsx index 9d7c479f..4ce396cf 100644 --- a/src/components/Editor/EditorToolbar/InsertLinkForm.tsx +++ b/src/components/Editor/EditorToolbar/InsertLinkForm.tsx @@ -1,6 +1,5 @@ import { Editor } from '@tiptap/core' import { createEffect, createSignal, onCleanup } from 'solid-js' - import { useLocalize } from '~/context/localize' import { validateUrl } from '~/utils/validate' import { InlineForm } from '../../_shared/InlineForm' diff --git a/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx b/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx deleted file mode 100644 index 1ea1d449..00000000 --- a/src/components/Editor/EditorToolbar/SimplifiedToolbar.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import clsx from 'clsx' -import { Show } from 'solid-js' -import { createEditorTransaction, useEditorHTML, useEditorIsEmpty } from 'solid-tiptap' -import { useEditorContext } from '~/context/editor' -import { useLocalize } from '~/context/localize' -import { useUI } from '~/context/ui' -import { Button } from '../../_shared/Button' -import { Icon } from '../../_shared/Icon' -import { Loading } from '../../_shared/Loading' -import { Popover } from '../../_shared/Popover' -import { SimplifiedEditorProps } from '../SimplifiedEditor' - -import styles from '../SimplifiedEditor.module.scss' - -export const ToolbarControls = ( - props: SimplifiedEditorProps & { setShouldShowLinkBubbleMenu: (x: boolean) => void } -) => { - const { t } = useLocalize() - const { showModal } = useUI() - const { editor } = useEditorContext() - const isActive = (name: string) => createEditorTransaction(editor, (ed) => ed?.isActive(name)) - const isBold = isActive('bold') - const isItalic = isActive('italic') - const isLink = isActive('link') - const isBlockquote = isActive('blockquote') - const isEmpty = useEditorIsEmpty(editor) - const html = useEditorHTML(editor) - - const handleClear = () => { - props.onCancel?.() - editor()?.commands.clearContent(true) - } - - const handleShowLinkBubble = () => { - editor()?.chain().focus().run() - props.setShouldShowLinkBubbleMenu(true) - } - - return ( - - {/* Only show controls if 'hideToolbar' is false */} -
-
- {/* Bold button */} - - {(triggerRef: (el: HTMLElement) => void) => ( - - )} - - {/* Italic button */} - - {(triggerRef) => ( - - )} - - {/* Link button */} - - {(triggerRef) => ( - - )} - - {/* Blockquote button (optional) */} - - - {(triggerRef) => ( - - )} - - - {/* Image button (optional) */} - - - {(triggerRef) => ( - - )} - - -
- {/* Cancel and submit buttons */} - -
- -
-
-
-
- ) -} diff --git a/src/components/Editor/EditorToolbar/ToolbarControl.tsx b/src/components/Editor/EditorToolbar/ToolbarControl.tsx index b4046e67..2abc7d09 100644 --- a/src/components/Editor/EditorToolbar/ToolbarControl.tsx +++ b/src/components/Editor/EditorToolbar/ToolbarControl.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx' import { JSX } from 'solid-js' import { Popover } from '~/components/_shared/Popover' -import styles from '../SimplifiedEditor.module.scss' +import styles from '../MiniEditor/MiniEditor.module.scss' interface ControlProps { editor: Editor diff --git a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss b/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss deleted file mode 100644 index 27f3ca77..00000000 --- a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.scss +++ /dev/null @@ -1,4 +0,0 @@ -.LinkBubbleMenu { - background: var(--editor-bubble-menu-background); - box-shadow: 0 4px 10px rgba(#000, 0.25); -} diff --git a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx b/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx deleted file mode 100644 index 2b87ab50..00000000 --- a/src/components/Editor/LinkBubbleMenu/LinkBubbleMenu.module.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { Editor } from '@tiptap/core' - -import { InsertLinkForm } from '../InsertLinkForm' - -import styles from './LinkBubbleMenu.module.scss' - -type Props = { - editor: Editor - ref: (el: HTMLDivElement) => void - onClose: () => void -} - -export const LinkBubbleMenuModule = (props: Props) => { - return ( -
- -
- ) -} diff --git a/src/components/Editor/LinkBubbleMenu/index.ts b/src/components/Editor/LinkBubbleMenu/index.ts deleted file mode 100644 index 7d3e3ace..00000000 --- a/src/components/Editor/LinkBubbleMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { LinkBubbleMenuModule } from './LinkBubbleMenu.module' diff --git a/src/components/Editor/MicroEditor/MicroEditor.tsx b/src/components/Editor/MicroEditor/MicroEditor.tsx index 549dd54a..907456e4 100644 --- a/src/components/Editor/MicroEditor/MicroEditor.tsx +++ b/src/components/Editor/MicroEditor/MicroEditor.tsx @@ -5,7 +5,7 @@ import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tip import { minimal } from '~/lib/editorExtensions' import { EditorToolbar } from '../EditorToolbar/EditorToolbar' -import styles from '../SimplifiedEditor.module.scss' +import styles from '../MiniEditor/MiniEditor.module.scss' interface MicroEditorProps { content?: string @@ -36,7 +36,7 @@ export const MicroEditor = (props: MicroEditorProps): JSX.Element => { createEffect(on(html, (c?: string) => c && props.onChange?.(c))) return ( -
+
diff --git a/src/components/Editor/SimplifiedEditor.module.scss b/src/components/Editor/MiniEditor/MiniEditor.module.scss similarity index 99% rename from src/components/Editor/SimplifiedEditor.module.scss rename to src/components/Editor/MiniEditor/MiniEditor.module.scss index cc9ba21d..f2f7ec2d 100644 --- a/src/components/Editor/SimplifiedEditor.module.scss +++ b/src/components/Editor/MiniEditor/MiniEditor.module.scss @@ -1,4 +1,4 @@ -.SimplifiedEditor { +.MiniEditor { width: 100%; display: flex; flex-direction: column; diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index 1b86aa82..e93f2289 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -3,12 +3,12 @@ import Placeholder from '@tiptap/extension-placeholder' import clsx from 'clsx' import { type JSX, Show, createEffect, createSignal, on } from 'solid-js' import { createEditorTransaction, createTiptapEditor, useEditorHTML } from 'solid-tiptap' +import { Button } from '~/components/_shared/Button' +import { useLocalize } from '~/context/localize' import { base } from '~/lib/editorExtensions' import { EditorToolbar } from '../EditorToolbar/EditorToolbar' -import { Button } from '~/components/_shared/Button' -import { useLocalize } from '~/context/localize' -import styles from '../SimplifiedEditor.module.scss' +import styles from './MiniEditor.module.scss' interface MiniEditorProps { content?: string @@ -58,7 +58,7 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element { } return ( -
+
diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx deleted file mode 100644 index f09c55c6..00000000 --- a/src/components/Editor/SimplifiedEditor.tsx +++ /dev/null @@ -1,244 +0,0 @@ -import { Editor, FocusPosition } from '@tiptap/core' -import { BubbleMenu } from '@tiptap/extension-bubble-menu' -import { CharacterCount } from '@tiptap/extension-character-count' -import { Placeholder } from '@tiptap/extension-placeholder' -import { clsx } from 'clsx' -import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' -import { Portal } from 'solid-js/web' -import { createEditorTransaction, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' -import { useEditorContext } from '~/context/editor' -import { useUI } from '~/context/ui' -import { base, custom } from '~/lib/editorExtensions' -import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' -import { UploadedFile } from '~/types/upload' -import { UploadModalContent } from '../Upload/UploadModalContent' -import { renderUploadedImage } from '../Upload/renderUploadedImage' -import { Modal } from '../_shared/Modal/Modal' -import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' -import { ToolbarControls } from './EditorToolbar/SimplifiedToolbar' -import { LinkBubbleMenuModule } from './LinkBubbleMenu' -import { TextBubbleMenu } from './TextBubbleMenu' - -import styles from './Editor.module.scss' - -export type SimplifiedEditorProps = { - placeholder: string - initialContent?: string - label?: string - onSubmit?: (text: string) => void - onCancel?: () => void - onChange?: (text: string) => void - variant?: 'minimal' | 'bordered' - maxLength?: number - noLimits?: boolean - maxHeight?: number - submitButtonText?: string - quoteEnabled?: boolean - imageEnabled?: boolean - setClear?: boolean - resetToInitial?: boolean - smallHeight?: boolean - submitByCtrlEnter?: boolean - hideToolbar?: boolean - controlsAlwaysVisible?: boolean - autoFocus?: boolean - isCancelButtonVisible?: boolean - isPosting?: boolean -} - -const DEFAULT_MAX_LENGTH = 400 - -const SimplifiedEditor = (props: SimplifiedEditorProps) => { - // local signals - const [counter, setCounter] = createSignal(0) - const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false) - const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) - const [editorElement, setEditorElement] = createSignal() - const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal() - const [linkBubbleMenuRef, setLinkBubbleMenuRef] = createSignal() - - // contexts - const { hideModal } = useUI() - const { editor, createEditor } = useEditorContext() - - const initEditor = (element?: HTMLElement) => { - if (element instanceof HTMLElement && editor()?.options.element !== element) { - const opts = { - element, - extensions: [ - // common extensions - ...base, - ...custom, - - // setup from component props - Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder }), - CharacterCount.configure({ limit: props.noLimits ? undefined : props.maxLength }), - - // bubble menu 1 - BubbleMenu.configure({ - pluginKey: 'bubble-menu', - element: textBubbleMenuRef(), - shouldShow: ({ view }) => view.hasFocus() && shouldShowTextBubbleMenu() - }), - - // bubble menu 2 - BubbleMenu.configure({ - pluginKey: 'bubble-link-input', - element: linkBubbleMenuRef(), - shouldShow: ({ state }) => !state.selection.empty && shouldShowLinkBubbleMenu(), - tippyOptions: { placement: 'bottom' } - }) - ], - editorProps: { - attributes: { class: styles.simplifiedEditorField } - }, - content: props.initialContent || '', - onCreate: () => console.info('[SimplifiedEditor] created'), - onContentError: console.error, - autofocus: (props.autoFocus && 'end') as FocusPosition | undefined, - editable: true, - enableCoreExtensions: true, - enableContentCheck: true, - injectNonce: undefined, // TODO: can be useful copyright/copyleft mark - parseOptions: undefined // see: https://prosemirror.net/docs/ref/#model.ParseOptions - } - - createEditor(opts) - } - } - - // editor observers - const isEmpty = useEditorIsEmpty(editor) - const isFocused = useEditorIsFocused(editor) - const selection = createEditorTransaction(editor, (ed) => ed?.state.selection) - const html = useEditorHTML(editor) - - /// EFFECTS /// - - // Mount event listeners for handling key events and clean up on component unmount - onMount(() => { - window.addEventListener('keydown', handleKeyDown) - onCleanup(() => { - window.removeEventListener('keydown', handleKeyDown) - editor()?.destroy() - }) - }) - - // watch changes - createEffect(on(editorElement, initEditor, { defer: true })) // element -> editorOptions -> set editor - createEffect( - on(selection, (s?: Editor['state']['selection']) => s && setShouldShowTextBubbleMenu(!s?.empty)) - ) - createEffect( - on( - () => props.setClear, - (x?: boolean) => x && editor()?.commands.clearContent(true) - ) - ) - createEffect( - on( - () => props.resetToInitial, - (x?: boolean) => x && editor()?.commands.setContent(props.initialContent || '') - ) - ) - createEffect(on([html, () => props.onChange], ([c, handler]) => c && handler && handler(c))) // onChange - createEffect(on(html, (c?: string) => c && setCounter(editor()?.storage.characterCount.characters()))) //counter - - /// HANDLERS /// - - const handleImageRender = (image?: UploadedFile) => { - image && renderUploadedImage(editor() as Editor, image) - hideModal() - } - - const handleKeyDown = (event: KeyboardEvent) => { - if ( - isFocused() && - !isEmpty() && - event.code === 'Enter' && - props.submitByCtrlEnter && - (event.metaKey || event.ctrlKey) - ) { - event.preventDefault() - props.onSubmit?.(html() || '') - } - } - - const handleHideLinkBubble = () => { - editor()?.commands.focus() - setShouldShowLinkBubbleMenu(false) - } - - useEscKeyDownHandler(handleHideLinkBubble) - - return ( - -
0 - })} - > - {/* Display label when applicable */} - 0}> -
{props.label}
-
- - - } - > - - - {/* Link bubble menu */} - - - - - - {/* editor element */} -
- - {/* Display character limit if maxLength is provided */} - -
{(props.maxLength || DEFAULT_MAX_LENGTH) - counter()}
-
- - {/* Image upload modal (show/hide) */} - - - - - - - -
- - ) -} - -export default SimplifiedEditor // Export component for lazy loading diff --git a/src/components/Editor/TextBubbleMenu/index.ts b/src/components/Editor/TextBubbleMenu/index.ts deleted file mode 100644 index 5dd90ad9..00000000 --- a/src/components/Editor/TextBubbleMenu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TextBubbleMenu } from './TextBubbleMenu' diff --git a/src/components/Editor/index.ts b/src/components/Editor/index.ts deleted file mode 100644 index 4f9a58ab..00000000 --- a/src/components/Editor/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { EditorComponent as Editor } from './Editor' -export { Panel } from './Panel' -export { TopicSelect } from '../TopicSelect' -export { UploadModalContent } from '../Upload/UploadModalContent' diff --git a/src/components/Views/EditView/EditSettingsView.tsx b/src/components/Views/EditView/EditSettingsView.tsx index 351c42c7..72f5802d 100644 --- a/src/components/Views/EditView/EditSettingsView.tsx +++ b/src/components/Views/EditView/EditSettingsView.tsx @@ -3,6 +3,7 @@ import deepEqual from 'fast-deep-equal' import { Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' +import { Panel } from '~/components/Editor/Panel/Panel' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' import { ShoutForm, useEditorContext } from '~/context/editor' @@ -12,7 +13,6 @@ import getMyShoutQuery from '~/graphql/query/core/article-my' import type { Shout, Topic } from '~/graphql/schema/core.gen' import { isDesktop } from '~/lib/mediaQuery' import { clone } from '~/utils/clone' -import { Panel } from '../../Editor' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { Modal } from '../../_shared/Modal' import { TableOfContents } from '../../_shared/TableOfContents' diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index 25dd9d7c..ba153c6e 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -3,6 +3,8 @@ import deepEqual from 'fast-deep-equal' import { Show, createEffect, createSignal, lazy, on, onCleanup, onMount } from 'solid-js' import { createStore } from 'solid-js/store' import { debounce } from 'throttle-debounce' +import { EditorComponent } from '~/components/Editor/Editor' +import { Panel } from '~/components/Editor/Panel/Panel' import { DropArea } from '~/components/_shared/DropArea' import { Icon } from '~/components/_shared/Icon' import { InviteMembers } from '~/components/_shared/InviteMembers' @@ -20,7 +22,6 @@ import { isDesktop } from '~/lib/mediaQuery' import { LayoutType } from '~/types/common' import { MediaItem } from '~/types/mediaitem' import { clone } from '~/utils/clone' -import { Editor as EditorComponent, Panel } from '../../Editor' import { AutoSaveNotice } from '../../Editor/AutoSaveNotice' import { AudioUploader } from '../../Upload/AudioUploader' import { VideoUploader } from '../../Upload/VideoUploader' diff --git a/src/components/Views/PublishSettings/PublishSettings.tsx b/src/components/Views/PublishSettings/PublishSettings.tsx index 9c848a52..552535b5 100644 --- a/src/components/Views/PublishSettings/PublishSettings.tsx +++ b/src/components/Views/PublishSettings/PublishSettings.tsx @@ -12,9 +12,10 @@ import { useTopics } from '~/context/topics' import { useSnackbar, useUI } from '~/context/ui' import { Topic } from '~/graphql/schema/core.gen' import { UploadedFile } from '~/types/upload' -import { TopicSelect, UploadModalContent } from '../../Editor' import { Modal } from '../../_shared/Modal' +import { TopicSelect } from '~/components/TopicSelect/TopicSelect' +import { UploadModalContent } from '~/components/Upload/UploadModalContent/UploadModalContent' import stylesBeside from '../../Feed/Beside.module.scss' import styles from './PublishSettings.module.scss' From fab8a5ed53f516a2b6c9967b664715b5ea147a66 Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 30 Sep 2024 13:38:44 +0300 Subject: [PATCH 22/23] minor --- src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx index 2ac02b7a..cde84f72 100644 --- a/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx @@ -19,8 +19,8 @@ export const FigureBubbleMenu = (props: Props) => { const { t } = useLocalize() const { hideModal } = useUI() - const handleUpload = (image: UploadedFile) => { - renderUploadedImage(props.editor, image) + const handleUpload = (image?: UploadedFile) => { + image && renderUploadedImage(props.editor, image) hideModal() } @@ -80,11 +80,7 @@ export const FigureBubbleMenu = (props: Props) => { - { - handleUpload(value as UploadedFile) - }} - /> +
) From 21b3903062bc3cdec67ff471203de59d43810b5c Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 30 Sep 2024 14:00:02 +0300 Subject: [PATCH 23/23] minor-fix --- src/components/Editor/BubbleMenu/TextBubbleMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx index 73d86c0b..e71b3575 100644 --- a/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx +++ b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx @@ -7,7 +7,7 @@ import { Popover } from '~/components/_shared/Popover' import { useLocalize } from '~/context/localize' import { InsertLinkForm } from '../EditorToolbar/InsertLinkForm' -import styles from '../TextBubbleMenu/TextBubbleMenu.module.scss' +import styles from './TextBubbleMenu.module.scss' const MiniEditor = lazy(() => import('../MiniEditor/MiniEditor'))