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 fd80e5f4..7b29f3ae 100644
--- a/app.config.ts
+++ b/app.config.ts
@@ -1,12 +1,12 @@
import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config'
-import viteConfig from './vite.config'
+import viteConfig, { isDev } from './vite.config'
-const isVercel = Boolean(process?.env.VERCEL)
-const isNetlify = Boolean(process?.env.NETLIFY)
+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: {
@@ -14,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-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..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.5.5",
+ "@types/node": "^22.6.1",
"@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": {
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
@@ -43,14 +41,12 @@ 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 } = 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) &&
@@ -107,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 = () => {
@@ -192,16 +186,11 @@ export const Comment = (props: Props) => {
}>
{t('Loading')}}>
- handleUpdate(value)}
- submitByCtrlEnter={true}
onCancel={() => setEditMode(false)}
- setClear={clearEditor()}
/>
@@ -261,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..4d666b5b 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,10 @@ export const CommentsTree = (props: Props) => {
}
>
- handleSubmitComment(value)}
- setClear={clearEditor()}
- isPosting={posting()}
- />
+
+
+
+
>
)
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/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/Editor/BubbleMenu/FigureBubbleMenu.tsx b/src/components/Editor/BubbleMenu/FigureBubbleMenu.tsx
index 706c9048..cde84f72 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 = {
@@ -20,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()
}
@@ -81,11 +80,7 @@ export const FigureBubbleMenu = (props: Props) => {
- {
- handleUpload(value as UploadedFile)
- }}
- />
+
)
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 97%
rename from src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx
rename to src/components/Editor/BubbleMenu/TextBubbleMenu.tsx
index 7d72ab0a..e71b3575 100644
--- a/src/components/Editor/TextBubbleMenu/TextBubbleMenu.tsx
+++ b/src/components/Editor/BubbleMenu/TextBubbleMenu.tsx
@@ -1,17 +1,15 @@
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'
-const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
+const MiniEditor = lazy(() => import('../MiniEditor/MiniEditor'))
type BubbleMenuProps = {
editor: Editor
@@ -146,18 +144,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/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.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 81d31163..b49b2799 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 { renderUploadedImage } from '../Upload/renderUploadedImage'
import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
+import { TextBubbleMenu } from './BubbleMenu/TextBubbleMenu'
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'
+import './Editor.module.scss'
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/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.tsx b/src/components/Editor/EditorToolbar.tsx
deleted file mode 100644
index ec4c2f2b..00000000
--- a/src/components/Editor/EditorToolbar.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/EditorToolbar.tsx b/src/components/Editor/EditorToolbar/EditorToolbar.tsx
new file mode 100644
index 00000000..3632bc29
--- /dev/null
+++ b/src/components/Editor/EditorToolbar/EditorToolbar.tsx
@@ -0,0 +1,138 @@
+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'
+import { ToolbarControl as Control } from './ToolbarControl'
+
+import styles from '../MiniEditor/MiniEditor.module.scss'
+
+interface EditorToolbarProps {
+ editor: Accessor
+ mode?: 'micro' | 'mini'
+}
+
+export const EditorToolbar = (props: EditorToolbarProps) => {
+ 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)
+
+ // 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
+ }
+ })
+ }
+ }
+ 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')}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ 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 64%
rename from src/components/Editor/InsertLinkForm/InsertLinkForm.tsx
rename to src/components/Editor/EditorToolbar/InsertLinkForm.tsx
index 379c3f5d..4ce396cf 100644
--- a/src/components/Editor/InsertLinkForm/InsertLinkForm.tsx
+++ b/src/components/Editor/EditorToolbar/InsertLinkForm.tsx
@@ -1,9 +1,8 @@
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'
-import { InlineForm } from '../InlineForm'
+import { InlineForm } from '../../_shared/InlineForm'
type Props = {
editor: Editor
@@ -21,12 +20,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 +48,9 @@ export const InsertLinkForm = (props: Props) => {
.focus()
.setLink({ href: checkUrl(value) })
.run()
+ props.onClose()
}
+
return (
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/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/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.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..907456e4
--- /dev/null
+++ b/src/components/Editor/MicroEditor/MicroEditor.tsx
@@ -0,0 +1,48 @@
+import Placeholder from '@tiptap/extension-placeholder'
+import clsx from 'clsx'
+import { type JSX, createEffect, createSignal, on } from 'solid-js'
+import { createTiptapEditor, useEditorHTML, useEditorIsFocused } from 'solid-tiptap'
+import { minimal } from '~/lib/editorExtensions'
+import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
+
+import styles from '../MiniEditor/MiniEditor.module.scss'
+
+interface MicroEditorProps {
+ content?: string
+ onChange?: (content: string) => void
+ onSubmit?: (content: string) => void
+ placeholder?: string
+}
+
+export const MicroEditor = (props: MicroEditorProps): JSX.Element => {
+ const [editorElement, setEditorElement] = createSignal()
+
+ const editor = createTiptapEditor(() => ({
+ element: editorElement()!,
+ extensions: [
+ ...minimal,
+ Placeholder.configure({ emptyNodeClass: styles.emptyNode, placeholder: props.placeholder })
+ ],
+ editorProps: {
+ attributes: {
+ class: styles.simplifiedEditorField
+ }
+ },
+ content: props.content || ''
+ }))
+
+ const isFocused = useEditorIsFocused(editor)
+ const html = useEditorHTML(editor)
+ createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
+
+ return (
+
+ )
+}
+
+export default MicroEditor
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 c866901f..e93f2289 100644
--- a/src/components/Editor/MiniEditor/MiniEditor.tsx
+++ b/src/components/Editor/MiniEditor/MiniEditor.tsx
@@ -1,71 +1,28 @@
-import type { Editor } from '@tiptap/core'
import CharacterCount from '@tiptap/extension-character-count'
import Placeholder from '@tiptap/extension-placeholder'
import clsx from 'clsx'
-import { type JSX, Show, createEffect, createSignal, onCleanup } from 'solid-js'
-import {
- createEditorTransaction,
- createTiptapEditor,
- useEditorHTML,
- useEditorIsEmpty,
- useEditorIsFocused
-} from 'solid-tiptap'
-import { Toolbar } from 'terracotta'
-import { Icon } from '~/components/_shared/Icon/Icon'
-import { Popover } from '~/components/_shared/Popover/Popover'
+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 { useUI } from '~/context/ui'
-import { base } from '~/lib/editorOptions'
-import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm'
+import { base } from '~/lib/editorExtensions'
+import { EditorToolbar } from '../EditorToolbar/EditorToolbar'
-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) => (
-
- )}
-
- )
-}
+import styles from './MiniEditor.module.scss'
interface MiniEditorProps {
content?: string
onChange?: (content: string) => void
+ onSubmit?: (content: string) => void
+ onCancel?: () => void
limit?: number
placeholder?: string
}
export default function MiniEditor(props: MiniEditorProps): JSX.Element {
+ const { t } = useLocalize()
const [editorElement, setEditorElement] = createSignal()
const [counter, setCounter] = createSignal(0)
- const [showLinkInput, setShowLinkInput] = createSignal(false)
- const [showSimpleMenu, setShowSimpleMenu] = createSignal(false)
- const { t } = useLocalize()
- const { showModal } = useUI()
const editor = createTiptapEditor(() => ({
element: editorElement()!,
@@ -82,12 +39,11 @@ 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 isFocused = createEditorTransaction(editor, (instance) => instance?.isFocused)
+ const isEmpty = createEditorTransaction(editor, (instance) => instance?.isEmpty)
const html = useEditorHTML(editor)
- createEffect(() => setShowSimpleMenu(isTextSelection()))
+ createEffect(on(html, (c?: string) => c && props.onChange?.(c)))
createEffect(() => {
const textLength = editor()?.getText().length || 0
@@ -96,88 +52,28 @@ export default function MiniEditor(props: MiniEditorProps): JSX.Element {
content && props.onChange?.(content)
})
- const handleLinkClick = () => {
- setShowLinkInput(!showLinkInput())
- editor()?.chain().focus().run()
+ const handleSubmit = () => {
+ html() && props.onSubmit?.(html() || '')
+ editor()?.commands.clearContent(true)
}
- // Prevent focus loss when clicking inside the toolbar
- const handleMouseDownOnToolbar = (event: MouseEvent) => {
- event.preventDefault() // Prevent the default focus shift
- }
- const [toolbarElement, setToolbarElement] = createSignal()
- // Attach the event handler to the toolbar
- onCleanup(() => {
- toolbarElement()?.removeEventListener('mousedown', handleMouseDownOnToolbar)
- })
return (
-
+
-
-
-
- {(instance) => (
-
-
setShowLinkInput(false)} />}
- >
-
- instance.chain().focus().toggleBold().run()}
- title={t('Bold')}
- >
-
-
- instance.chain().focus().toggleItalic().run()}
- title={t('Italic')}
- >
-
-
-
-
-
- instance.chain().focus().toggleBlockquote().run()}
- title={t('Add blockquote')}
- >
-
-
- showModal('simplifiedEditorUploadImage')}
- title={t('Add image')}
- >
-
-
-
-
-
- )}
-
-
-
-
+
+
+
+
+
0}>
{counter()} / {props.limit || '∞'}
diff --git a/src/components/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'
- }
-}
diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx
deleted file mode 100644
index 2f689753..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/editorOptions'
-import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler'
-import { UploadedFile } from '~/types/upload'
-import { Modal } from '../_shared/Modal/Modal'
-import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
-import { ToolbarControls } from './EditorToolbar'
-import { LinkBubbleMenuModule } from './LinkBubbleMenu'
-import { TextBubbleMenu } from './TextBubbleMenu'
-import { UploadModalContent } from './UploadModalContent'
-import { renderUploadedImage } from './renderUploadedImage'
-
-import styles from './SimplifiedEditor.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/TopicSelect/TopicSelect.module.scss b/src/components/Editor/TopicSelect/TopicSelect.module.scss
deleted file mode 100644
index cbbdc062..00000000
--- a/src/components/Editor/TopicSelect/TopicSelect.module.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-.selectedItem {
- cursor: pointer;
-
- &.mainTopic {
- cursor: default;
-
- &,
- + :global(.solid-select-multi-value-remove) {
- color: #ccc;
- }
-
- &::before {
- background: #000;
- content: '';
- height: 100%;
- left: 0;
- position: absolute;
- top: 0;
- width: 100%;
- z-index: -1;
- }
- }
-}
-
-.TopicSelect .solid-select-list {
- background: #fff;
- position: relative;
- z-index: 13;
-}
-
-.TopicSelect .solid-select-option[data-disabled='true'] {
- display: none;
-}
diff --git a/src/components/Editor/index.ts b/src/components/Editor/index.ts
deleted file mode 100644
index e2327288..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 './UploadModalContent'
diff --git a/src/components/TopicSelect/TopicSelect.module.scss b/src/components/TopicSelect/TopicSelect.module.scss
new file mode 100644
index 00000000..1d8c1a19
--- /dev/null
+++ b/src/components/TopicSelect/TopicSelect.module.scss
@@ -0,0 +1,73 @@
+.selectedItem {
+ cursor: pointer;
+
+ &.mainTopic {
+ cursor: default;
+
+ &,
+ + :global(.solid-select-multi-value-remove) {
+ color: #ccc;
+ }
+
+ &::before {
+ background: #000;
+ content: '';
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ z-index: -1;
+ }
+ }
+}
+
+.TopicSelect .solid-select-list {
+ background: #fff;
+ position: relative;
+ z-index: 13;
+}
+
+.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 rgb(0 0 0 / 10%);
+}
+
+.option {
+ padding: 10px;
+ cursor: pointer;
+}
+
+.disabled {
+ color: gray;
+ pointer-events: none;
+}
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/Author/Author.tsx b/src/components/Views/Author/Author.tsx
index f465f1d8..d70d870b 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..72f5802d 100644
--- a/src/components/Views/EditView/EditSettingsView.tsx
+++ b/src/components/Views/EditView/EditSettingsView.tsx
@@ -1,20 +1,18 @@
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 { Panel } from '~/components/Editor/Panel/Panel'
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'
import { clone } from '~/utils/clone'
-import { Panel } from '../../Editor'
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
import { Modal } from '../../_shared/Modal'
import { TableOfContents } from '../../_shared/TableOfContents'
@@ -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 db2a4edd..ba153c6e 100644
--- a/src/components/Views/EditView/EditView.tsx
+++ b/src/components/Views/EditView/EditView.tsx
@@ -1,29 +1,19 @@
import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal'
-import {
- Accessor,
- 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 { 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'
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'
@@ -32,15 +22,14 @@ 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 { 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 = {
@@ -65,10 +54,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 { client } = useSession()
const {
form,
formErrors,
@@ -78,14 +64,18 @@ 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(props.shout)
+ const [mediaItems, setMediaItems] = createSignal([])
+
+ createEffect(() => setMediaItems(JSON.parse(form.media || '[]')))
createEffect(
on(
@@ -97,7 +87,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 +121,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 +257,7 @@ export const EditView = (props: Props) => {
const showSubtitleInput = () => {
setIsSubtitleVisible(true)
- subtitleInput?.focus()
+ subtitleInput()?.focus()
}
const showLeadInput = () => {
@@ -359,7 +349,7 @@ export const EditView = (props: Props) => {
(subtitleInput = el)}
+ textAreaRef={setSubtitleInput}
allowEnterKey={false}
value={(value) => handleInputChange('subtitle', value || '')}
class={styles.subtitleInput}
@@ -369,13 +359,10 @@ export const EditView = (props: Props) => {
/>
- handleInputChange('lead', value)}
+ content={form.lead}
+ onChange={(value: string) => handleInputChange('lead', value)}
/>
@@ -455,7 +442,7 @@ export const EditView = (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([])
- 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 layouts = props.layout ? [props.layout] : EXPO_LAYOUTS
const options: LoadShoutsOptions = {
- filters: { layouts: layouts(), featured: true },
+ filters: { layouts, featured: true },
limit: 10,
random_limit: 100
}
@@ -61,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
}
@@ -73,127 +50,46 @@ export const Expo = (props: Props) => {
setReactedTopMonthArticles(resp?.data?.load_shouts_random_top || [])
}
- onMount(() => {
- loadRandomTopArticles()
- loadRandomTopMonthArticles()
- })
-
+ // Эффект для загрузки random top при изменении layout
createEffect(
on(
() => props.layout,
- () => {
- setExpoShouts([])
- setFavoriteTopArticles([])
- setReactedTopMonthArticles([])
- loadRandomTopArticles()
- loadRandomTopMonthArticles()
+ async (_layout?: ExpoLayoutType) => {
+ await loadRandomTopArticles()
+ await loadRandomTopMonthArticles()
}
)
)
- onCleanup(() => {
- setExpoShouts([])
- })
- const ExpoTabs = () => (
-
- )
- const ExpoGrid = () => (
-
-
-
- {(shout) => (
-
- )}
-
-
0} keyed={true}>
-
-
-
- {(shout) => (
-
- )}
-
-
0} keyed={true}>
-
-
-
- {(shout) => (
-
- )}
-
-
-
- )
-
return (
-
+
} keyed>
+ {(feed: Shout[]) => (
+
+
+
+ {(shout) => (
+
+ )}
+
+
-
0} fallback={}>
-
-
-
+ 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/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx
index b083104d..3c05557d 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()
}
@@ -218,7 +215,7 @@ export const FeedView = (props: FeedProps) => {
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')}
-
+
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 85%
rename from src/components/Editor/InlineForm/InlineForm.tsx
rename to src/components/_shared/InlineForm/InlineForm.tsx
index 2eaf983f..4325832f 100644
--- a/src/components/Editor/InlineForm/InlineForm.tsx
+++ b/src/components/_shared/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 (