prestorybook
This commit is contained in:
parent
8824fbab2f
commit
ad4bda3c24
|
@ -7,19 +7,27 @@ const config: StorybookConfig = {
|
||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
'@storybook/addon-interactions',
|
'@storybook/addon-interactions',
|
||||||
'@storybook/addon-a11y',
|
'@storybook/addon-a11y',
|
||||||
'@storybook/addon-themes'
|
'@storybook/addon-themes',
|
||||||
|
'@storybook/addon-style-config'
|
||||||
],
|
],
|
||||||
framework: {
|
framework: {
|
||||||
name: 'storybook-solidjs-vite',
|
name: 'storybook-solidjs-vite',
|
||||||
options: {
|
options: {
|
||||||
builder: {
|
builder: {
|
||||||
viteConfigPath: './app.config.ts'
|
viteConfigPath: './vite.config.ts'
|
||||||
}
|
}
|
||||||
} as FrameworkOptions
|
} as FrameworkOptions
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
autodocs: 'tag'
|
autodocs: 'tag'
|
||||||
},
|
},
|
||||||
|
viteFinal: (config) => {
|
||||||
|
if (config.build) {
|
||||||
|
config.build.sourcemap = true
|
||||||
|
config.build.minify = process.env.NODE_ENV === 'production'
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
previewHead: (head) => `
|
previewHead: (head) => `
|
||||||
${head}
|
${head}
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { withThemeByClassName } from '@storybook/addon-themes'
|
import { withThemeByClassName } from '@storybook/addon-themes'
|
||||||
import '../src/styles/_imports.scss'
|
|
||||||
|
|
||||||
const preview = {
|
const preview = {
|
||||||
parameters: {
|
parameters: {
|
||||||
|
|
38
api/jsonify.js
Normal file
38
api/jsonify.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// api/convert.js
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import { base, custom } from 'src/lib/editorOptions'
|
||||||
|
|
||||||
|
// Добавьте другие расширения при необходимости
|
||||||
|
|
||||||
|
export default function handler(req, res) {
|
||||||
|
// Разрешаем только метод POST
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
res.status(405).json({ error: 'Method not allowed' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем HTML из тела запроса
|
||||||
|
const { html } = req.body
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
res.status(400).json({ error: 'No HTML content provided' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const editor = new Editor({ extensions: [...base, ...custom] })
|
||||||
|
|
||||||
|
editor.commands.setContent(html, false, {
|
||||||
|
parseOptions: {
|
||||||
|
preserveWhitespace: 'full'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const jsonOutput = editor.getJSON()
|
||||||
|
|
||||||
|
res.status(200).json(jsonOutput)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при конвертации:', error)
|
||||||
|
res.status(500).json({ error: 'Internal Server Error' })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,5 @@
|
||||||
import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config'
|
import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config'
|
||||||
import { CSSOptions } from 'vite'
|
import viteConfig, { runtime } from './vite.config'
|
||||||
// import { visualizer } from 'rollup-plugin-visualizer'
|
|
||||||
import mkcert from 'vite-plugin-mkcert'
|
|
||||||
import { PolyfillOptions, nodePolyfills } from 'vite-plugin-node-polyfills'
|
|
||||||
import sassDts from 'vite-plugin-sass-dts'
|
|
||||||
|
|
||||||
const isVercel = Boolean(process?.env.VERCEL)
|
|
||||||
const isNetlify = Boolean(process?.env.NETLIFY)
|
|
||||||
const isBun = Boolean(process.env.BUN)
|
|
||||||
const runtime = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node'
|
|
||||||
console.info(`[app.config] build for ${runtime}!`)
|
|
||||||
|
|
||||||
const polyfillOptions = {
|
|
||||||
include: ['path', 'stream', 'util'],
|
|
||||||
exclude: ['http'],
|
|
||||||
globals: {
|
|
||||||
Buffer: true
|
|
||||||
},
|
|
||||||
overrides: {
|
|
||||||
fs: 'memfs'
|
|
||||||
},
|
|
||||||
protocolImports: true
|
|
||||||
} as PolyfillOptions
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
nitro: {
|
nitro: {
|
||||||
|
@ -34,31 +12,5 @@ export default defineConfig({
|
||||||
https: true
|
https: true
|
||||||
},
|
},
|
||||||
devOverlay: true,
|
devOverlay: true,
|
||||||
vite: {
|
vite: viteConfig
|
||||||
envPrefix: 'PUBLIC_',
|
|
||||||
plugins: [!isVercel && mkcert(), nodePolyfills(polyfillOptions), sassDts()],
|
|
||||||
css: {
|
|
||||||
preprocessorOptions: {
|
|
||||||
scss: {
|
|
||||||
additionalData: '@import "src/styles/imports";\n',
|
|
||||||
includePaths: ['./public', './src/styles']
|
|
||||||
}
|
|
||||||
} as CSSOptions['preprocessorOptions']
|
|
||||||
},
|
|
||||||
build: {
|
|
||||||
target: 'esnext',
|
|
||||||
sourcemap: true,
|
|
||||||
rollupOptions: {
|
|
||||||
// plugins: [visualizer()]
|
|
||||||
output: {
|
|
||||||
manualChunks: {
|
|
||||||
icons: ['./src/components/_shared/Icon/Icon.tsx'],
|
|
||||||
session: ['./src/context/session.tsx'],
|
|
||||||
editor: ['./src/context/editor.tsx'],
|
|
||||||
connect: ['./src/context/connect.tsx']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as SolidStartInlineConfig)
|
} as SolidStartInlineConfig)
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
"noBarrelFile": "off"
|
"noBarrelFile": "off"
|
||||||
},
|
},
|
||||||
"style": {
|
"style": {
|
||||||
|
"noNonNullAssertion": "off",
|
||||||
"noNamespaceImport": "warn",
|
"noNamespaceImport": "warn",
|
||||||
"useBlockStatements": "off",
|
"useBlockStatements": "off",
|
||||||
"noImplicitBoolean": "off",
|
"noImplicitBoolean": "off",
|
||||||
|
|
11
package.json
11
package.json
|
@ -82,9 +82,10 @@
|
||||||
"@tiptap/extension-text": "^2.6.6",
|
"@tiptap/extension-text": "^2.6.6",
|
||||||
"@tiptap/extension-underline": "^2.6.6",
|
"@tiptap/extension-underline": "^2.6.6",
|
||||||
"@tiptap/extension-youtube": "^2.6.6",
|
"@tiptap/extension-youtube": "^2.6.6",
|
||||||
|
"@tiptap/starter-kit": "^2.6.6",
|
||||||
"@types/cookie": "^0.6.0",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/cookie-signature": "^1.1.2",
|
"@types/cookie-signature": "^1.1.2",
|
||||||
"@types/node": "^22.5.2",
|
"@types/node": "^22.5.3",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@urql/core": "^5.0.6",
|
"@urql/core": "^5.0.6",
|
||||||
"axe-playwright": "^2.0.2",
|
"axe-playwright": "^2.0.2",
|
||||||
|
@ -104,12 +105,12 @@
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"prosemirror-history": "^1.4.1",
|
"prosemirror-history": "^1.4.1",
|
||||||
"prosemirror-trailing-node": "^2.0.9",
|
"prosemirror-trailing-node": "^2.0.9",
|
||||||
"prosemirror-view": "^1.34.1",
|
"prosemirror-view": "^1.34.2",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"sass": "1.76.0",
|
"sass": "1.76.0",
|
||||||
"solid-js": "^1.8.22",
|
"solid-js": "^1.8.22",
|
||||||
"solid-popper": "^0.3.0",
|
"solid-popper": "^0.3.0",
|
||||||
"solid-tiptap": "0.7.0",
|
"solid-tiptap": "^0.7.0",
|
||||||
"solid-transition-group": "^0.2.3",
|
"solid-transition-group": "^0.2.3",
|
||||||
"storybook": "^8.2.9",
|
"storybook": "^8.2.9",
|
||||||
"storybook-solidjs": "^1.0.0-beta.2",
|
"storybook-solidjs": "^1.0.0-beta.2",
|
||||||
|
@ -120,6 +121,7 @@
|
||||||
"stylelint-order": "^6.0.4",
|
"stylelint-order": "^6.0.4",
|
||||||
"stylelint-scss": "^6.5.1",
|
"stylelint-scss": "^6.5.1",
|
||||||
"swiper": "^11.1.12",
|
"swiper": "^11.1.12",
|
||||||
|
"terracotta": "^1.0.5",
|
||||||
"throttle-debounce": "^5.0.2",
|
"throttle-debounce": "^5.0.2",
|
||||||
"tslib": "^2.7.0",
|
"tslib": "^2.7.0",
|
||||||
"typescript": "^5.5.4",
|
"typescript": "^5.5.4",
|
||||||
|
@ -134,7 +136,8 @@
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"yjs": "13.6.18",
|
"yjs": "13.6.18",
|
||||||
"y-prosemirror": "1.2.12"
|
"y-prosemirror": "1.2.12",
|
||||||
|
"prosemirror-view": "1.34.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 20"
|
"node": ">= 20"
|
||||||
|
|
|
@ -12,11 +12,16 @@ import { SessionProvider } from './context/session'
|
||||||
import { TopicsProvider } from './context/topics'
|
import { TopicsProvider } from './context/topics'
|
||||||
import { UIProvider } from './context/ui' // snackbar included
|
import { UIProvider } from './context/ui' // snackbar included
|
||||||
import '~/styles/app.scss'
|
import '~/styles/app.scss'
|
||||||
|
import { AuthToken } from '@authorizerdev/authorizer-js'
|
||||||
|
|
||||||
export const Providers = (props: { children?: JSX.Element }) => {
|
export const Providers = (props: { children?: JSX.Element }) => {
|
||||||
|
const sessionStateChanged = (payload: AuthToken) => {
|
||||||
|
console.debug(payload)
|
||||||
|
// TODO: maybe load subs here
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<LocalizeProvider>
|
<LocalizeProvider>
|
||||||
<SessionProvider onStateChangeCallback={console.info}>
|
<SessionProvider onStateChangeCallback={sessionStateChanged}>
|
||||||
<TopicsProvider>
|
<TopicsProvider>
|
||||||
<FeedProvider>
|
<FeedProvider>
|
||||||
<MetaProvider>
|
<MetaProvider>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export const CommentsTree = (props: Props) => {
|
||||||
const { reactionEntities, createShoutReaction, loadReactionsBy } = useReactions()
|
const { reactionEntities, createShoutReaction, loadReactionsBy } = useReactions()
|
||||||
|
|
||||||
const comments = createMemo(() =>
|
const comments = createMemo(() =>
|
||||||
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT')
|
Object.values(reactionEntities()).filter((reaction) => reaction.kind === 'COMMENT')
|
||||||
)
|
)
|
||||||
|
|
||||||
const sortedComments = createMemo(() => {
|
const sortedComments = createMemo(() => {
|
||||||
|
@ -158,11 +158,11 @@ export const CommentsTree = (props: Props) => {
|
||||||
<SimplifiedEditor
|
<SimplifiedEditor
|
||||||
quoteEnabled={true}
|
quoteEnabled={true}
|
||||||
imageEnabled={true}
|
imageEnabled={true}
|
||||||
autoFocus={false}
|
options={{ autofocus: false }}
|
||||||
submitByCtrlEnter={true}
|
submitByCtrlEnter={true}
|
||||||
placeholder={t('Write a comment...')}
|
placeholder={t('Write a comment...')}
|
||||||
onSubmit={(value) => handleSubmitComment(value)}
|
onSubmit={(value) => handleSubmitComment(value)}
|
||||||
setClear={clearEditor()}
|
reset={clearEditor()}
|
||||||
isPosting={posting()}
|
isPosting={posting()}
|
||||||
/>
|
/>
|
||||||
</ShowIfAuthenticated>
|
</ShowIfAuthenticated>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
const [isLoading, setIsLoading] = createSignal(false)
|
const [isLoading, setIsLoading] = createSignal(false)
|
||||||
|
|
||||||
const checkReaction = (reactionKind: ReactionKind) =>
|
const checkReaction = (reactionKind: ReactionKind) =>
|
||||||
Object.values(reactionEntities).some(
|
Object.values(reactionEntities()).some(
|
||||||
(r) =>
|
(r) =>
|
||||||
r.kind === reactionKind &&
|
r.kind === reactionKind &&
|
||||||
r.created_by.id === author()?.id &&
|
r.created_by.id === author()?.id &&
|
||||||
|
@ -38,7 +38,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
|
||||||
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
|
||||||
|
|
||||||
const shoutRatingReactions = createMemo(() =>
|
const shoutRatingReactions = createMemo(() =>
|
||||||
Object.values(reactionEntities).filter(
|
Object.values(reactionEntities()).filter(
|
||||||
(r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to
|
(r) => ['LIKE', 'DISLIKE'].includes(r.kind) && r.shout.id === props.shout.id && !r.reply_to
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -29,17 +29,16 @@ import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'sol
|
||||||
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
|
||||||
import uniqolor from 'uniqolor'
|
import uniqolor from 'uniqolor'
|
||||||
import { Doc } from 'yjs'
|
import { Doc } from 'yjs'
|
||||||
|
|
||||||
import { useEditorContext } from '~/context/editor'
|
import { useEditorContext } from '~/context/editor'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useSession } from '~/context/session'
|
import { useSession } from '~/context/session'
|
||||||
import { useSnackbar } from '~/context/ui'
|
import { useSnackbar } from '~/context/ui'
|
||||||
|
import { Author } from '~/graphql/schema/core.gen'
|
||||||
import { handleImageUpload } from '~/lib/handleImageUpload'
|
import { handleImageUpload } from '~/lib/handleImageUpload'
|
||||||
|
|
||||||
import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
|
||||||
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
import { EditorFloatingMenu } from './EditorFloatingMenu'
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import Article from './extensions/Article'
|
import { ArticleNode } from './extensions/Article'
|
||||||
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
import { CustomBlockquote } from './extensions/CustomBlockquote'
|
||||||
import { Figcaption } from './extensions/Figcaption'
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
|
@ -50,7 +49,7 @@ import { ToggleTextWrap } from './extensions/ToggleTextWrap'
|
||||||
import { TrailingNode } from './extensions/TrailingNode'
|
import { TrailingNode } from './extensions/TrailingNode'
|
||||||
|
|
||||||
import './Prosemirror.scss'
|
import './Prosemirror.scss'
|
||||||
import { Author } from '~/graphql/schema/core.gen'
|
import { renderUploadedImage } from './renderUploadedImage'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
shoutId: number
|
shoutId: number
|
||||||
|
@ -124,26 +123,8 @@ export const EditorComponent = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
showSnackbar({ body: t('Uploading image') })
|
showSnackbar({ body: t('Uploading image') })
|
||||||
const result = await handleImageUpload(uplFile, session()?.access_token || '')
|
const image = await handleImageUpload(uplFile, session()?.access_token || '')
|
||||||
|
renderUploadedImage(editor() as Editor, image)
|
||||||
editor()
|
|
||||||
?.chain()
|
|
||||||
.focus()
|
|
||||||
.insertContent({
|
|
||||||
type: 'figure',
|
|
||||||
attrs: { 'data-type': 'image' },
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'image',
|
|
||||||
attrs: { src: result.url }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'figcaption',
|
|
||||||
content: [{ type: 'text', text: result.originalFilename }]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Paste Image Error]:', error)
|
console.error('[Paste Image Error]:', error)
|
||||||
}
|
}
|
||||||
|
@ -293,7 +274,7 @@ export const EditorComponent = (props: Props) => {
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
TrailingNode,
|
TrailingNode,
|
||||||
Article
|
ArticleNode
|
||||||
],
|
],
|
||||||
onTransaction: ({ transaction }) => {
|
onTransaction: ({ transaction }) => {
|
||||||
if (transaction.docChanged) {
|
if (transaction.docChanged) {
|
||||||
|
|
|
@ -40,7 +40,6 @@ export const InsertLinkForm = (props: Props) => {
|
||||||
.setLink({ href: checkUrl(value) })
|
.setLink({ href: checkUrl(value) })
|
||||||
.run()
|
.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<InlineForm
|
<InlineForm
|
||||||
|
@ -49,7 +48,7 @@ export const InsertLinkForm = (props: Props) => {
|
||||||
onClear={handleClearLinkForm}
|
onClear={handleClearLinkForm}
|
||||||
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))}
|
validate={(value) => (validateUrl(value) ? '' : t('Invalid url format'))}
|
||||||
onSubmit={handleLinkFormSubmit}
|
onSubmit={handleLinkFormSubmit}
|
||||||
onClose={() => props.onClose()}
|
onClose={props.onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Editor } from '@tiptap/core'
|
|
||||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||||
import { Bold } from '@tiptap/extension-bold'
|
import { Bold } from '@tiptap/extension-bold'
|
||||||
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
|
||||||
|
@ -11,7 +10,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'
|
||||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||||
import { Text } from '@tiptap/extension-text'
|
import { Text } from '@tiptap/extension-text'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createReaction, createSignal, on, onCleanup, onMount } from 'solid-js'
|
import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
|
||||||
import { Portal } from 'solid-js/web'
|
import { Portal } from 'solid-js/web'
|
||||||
import {
|
import {
|
||||||
createEditorTransaction,
|
createEditorTransaction,
|
||||||
|
@ -20,23 +19,26 @@ import {
|
||||||
useEditorIsEmpty,
|
useEditorIsEmpty,
|
||||||
useEditorIsFocused
|
useEditorIsFocused
|
||||||
} from 'solid-tiptap'
|
} from 'solid-tiptap'
|
||||||
|
|
||||||
import { useEditorContext } from '~/context/editor'
|
import { useEditorContext } from '~/context/editor'
|
||||||
import { useLocalize } from '~/context/localize'
|
import { useLocalize } from '~/context/localize'
|
||||||
import { useUI } from '~/context/ui'
|
|
||||||
import { UploadedFile } from '~/types/upload'
|
import { UploadedFile } from '~/types/upload'
|
||||||
import { Button } from '../_shared/Button'
|
import { Button } from '../_shared/Button'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { Loading } from '../_shared/Loading'
|
import { Loading } from '../_shared/Loading'
|
||||||
import { Modal } from '../_shared/Modal'
|
|
||||||
import { Popover } from '../_shared/Popover'
|
import { Popover } from '../_shared/Popover'
|
||||||
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
|
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
|
||||||
import styles from './SimplifiedEditor.module.scss'
|
|
||||||
import { TextBubbleMenu } from './TextBubbleMenu'
|
import { TextBubbleMenu } from './TextBubbleMenu'
|
||||||
import { UploadModalContent } from './UploadModalContent'
|
import { UploadModalContent } from './UploadModalContent'
|
||||||
import { Figcaption } from './extensions/Figcaption'
|
import { Figcaption } from './extensions/Figcaption'
|
||||||
import { Figure } from './extensions/Figure'
|
import { Figure } from './extensions/Figure'
|
||||||
|
|
||||||
|
import { Editor } from '@tiptap/core'
|
||||||
|
import { useUI } from '~/context/ui'
|
||||||
|
import { Modal } from '../_shared/Modal/Modal'
|
||||||
|
import styles from './SimplifiedEditor.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
placeholder: string
|
placeholder: string
|
||||||
initialContent?: string
|
initialContent?: string
|
||||||
|
@ -69,27 +71,103 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
const { showModal, hideModal } = useUI()
|
const { showModal, hideModal } = useUI()
|
||||||
const [counter, setCounter] = createSignal<number>(0)
|
const [counter, setCounter] = createSignal<number>(0)
|
||||||
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
|
const [shouldShowLinkBubbleMenu, setShouldShowLinkBubbleMenu] = createSignal(false)
|
||||||
|
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
|
||||||
|
const [editorElement, setEditorElement] = createSignal<HTMLDivElement>()
|
||||||
const { editor, setEditor } = useEditorContext()
|
const { editor, setEditor } = useEditorContext()
|
||||||
|
|
||||||
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
|
const maxLength = props.maxLength ?? DEFAULT_MAX_LENGTH
|
||||||
let editorEl: HTMLDivElement | undefined
|
|
||||||
let wrapperEditorElRef: HTMLElement | undefined
|
let wrapperEditorElRef: HTMLElement | undefined
|
||||||
let textBubbleMenuRef: HTMLDivElement | undefined
|
let textBubbleMenuRef: HTMLDivElement | undefined
|
||||||
let linkBubbleMenuRef: HTMLDivElement | undefined
|
let linkBubbleMenuRef: HTMLDivElement | undefined
|
||||||
|
|
||||||
// Extend the Figure extension to include Figcaption
|
|
||||||
const ImageFigure = Figure.extend({
|
const ImageFigure = Figure.extend({
|
||||||
name: 'capturedImage',
|
name: 'capturedImage',
|
||||||
content: 'figcaption image'
|
content: 'figcaption image'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(
|
||||||
|
on(
|
||||||
|
() => editorElement(),
|
||||||
|
(ee: HTMLDivElement | undefined) => {
|
||||||
|
if (ee && textBubbleMenuRef && linkBubbleMenuRef) {
|
||||||
|
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
|
||||||
|
element: ee,
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class: styles.simplifiedEditorField
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extensions: [
|
||||||
|
Document,
|
||||||
|
Text,
|
||||||
|
Paragraph,
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Link.extend({
|
||||||
|
inclusive: false
|
||||||
|
}).configure({
|
||||||
|
autolink: true,
|
||||||
|
openOnClick: false
|
||||||
|
}),
|
||||||
|
CharacterCount.configure({
|
||||||
|
limit: props.noLimits ? null : maxLength
|
||||||
|
}),
|
||||||
|
Blockquote.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: styles.blockQuote
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'textBubbleMenu',
|
||||||
|
element: textBubbleMenuRef,
|
||||||
|
shouldShow: ({ view, state }) => {
|
||||||
|
if (!props.onlyBubbleControls) return false
|
||||||
|
const { selection } = state
|
||||||
|
const { empty } = selection
|
||||||
|
return view.hasFocus() && !empty
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
BubbleMenu.configure({
|
||||||
|
pluginKey: 'linkBubbleMenu',
|
||||||
|
element: linkBubbleMenuRef,
|
||||||
|
shouldShow: ({ state }) => {
|
||||||
|
const { selection } = state
|
||||||
|
const { empty } = selection
|
||||||
|
return !empty && shouldShowLinkBubbleMenu()
|
||||||
|
},
|
||||||
|
tippyOptions: {
|
||||||
|
placement: 'bottom'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ImageFigure,
|
||||||
|
Image,
|
||||||
|
Figcaption,
|
||||||
|
Placeholder.configure({
|
||||||
|
emptyNodeClass: styles.emptyNode,
|
||||||
|
placeholder: props.placeholder
|
||||||
|
})
|
||||||
|
],
|
||||||
|
autofocus: props.autoFocus,
|
||||||
|
content: props.initialContent || null
|
||||||
|
}))
|
||||||
|
const editorInstance = freshEditor()
|
||||||
|
if (!editorInstance) return
|
||||||
|
setEditor(editorInstance)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ defer: true }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const isEmpty = useEditorIsEmpty(() => editor())
|
const isEmpty = useEditorIsEmpty(() => editor())
|
||||||
const isFocused = useEditorIsFocused(() => editor())
|
const isFocused = useEditorIsFocused(() => editor())
|
||||||
|
|
||||||
const isActive = (name: string) =>
|
const isActive = (name: string) =>
|
||||||
createEditorTransaction(
|
createEditorTransaction(
|
||||||
() => editor(),
|
() => editor(),
|
||||||
(ed) => ed?.isActive(name)
|
(ed) => {
|
||||||
|
return ed?.isActive(name)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const html = useEditorHTML(() => editor())
|
const html = useEditorHTML(() => editor())
|
||||||
|
@ -127,6 +205,16 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
editor()?.commands.clearContent(true)
|
editor()?.commands.clearContent(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.setClear) {
|
||||||
|
editor()?.commands.clearContent(true)
|
||||||
|
}
|
||||||
|
if (props.resetToInitial) {
|
||||||
|
editor()?.commands.clearContent(true)
|
||||||
|
if (props.initialContent) editor()?.commands.setContent(props.initialContent)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const handleKeyDown = (event: KeyboardEvent) => {
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
if (isEmpty() || !isFocused()) {
|
if (isEmpty() || !isFocused()) {
|
||||||
return
|
return
|
||||||
|
@ -155,89 +243,19 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
window.removeEventListener('keydown', handleKeyDown)
|
window.removeEventListener('keydown', handleKeyDown)
|
||||||
editor()?.destroy()
|
editor()?.destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug('[SimplifiedEditor] mounted')
|
|
||||||
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
|
|
||||||
element: editorEl as HTMLDivElement,
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: styles.simplifiedEditorField
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extensions: [
|
|
||||||
Document,
|
|
||||||
Text,
|
|
||||||
Paragraph,
|
|
||||||
Bold,
|
|
||||||
Italic,
|
|
||||||
Link.extend({
|
|
||||||
inclusive: false
|
|
||||||
}).configure({
|
|
||||||
autolink: true,
|
|
||||||
openOnClick: false
|
|
||||||
}),
|
|
||||||
CharacterCount.configure({
|
|
||||||
limit: props.noLimits ? null : maxLength
|
|
||||||
}),
|
|
||||||
Blockquote.configure({
|
|
||||||
HTMLAttributes: {
|
|
||||||
class: styles.blockQuote
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'textBubbleMenu',
|
|
||||||
element: textBubbleMenuRef,
|
|
||||||
shouldShow: ({ view, state }) => {
|
|
||||||
if (!props.onlyBubbleControls) return false
|
|
||||||
const { selection } = state
|
|
||||||
return view.hasFocus() && !selection.empty
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
BubbleMenu.configure({
|
|
||||||
pluginKey: 'linkBubbleMenu',
|
|
||||||
element: linkBubbleMenuRef,
|
|
||||||
shouldShow: ({ state }) =>
|
|
||||||
state.selection && !state.selection.empty && shouldShowLinkBubbleMenu(),
|
|
||||||
tippyOptions: {
|
|
||||||
placement: 'bottom'
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
ImageFigure,
|
|
||||||
Image,
|
|
||||||
Figcaption,
|
|
||||||
Placeholder.configure({
|
|
||||||
emptyNodeClass: styles.emptyNode,
|
|
||||||
placeholder: props.placeholder
|
|
||||||
})
|
|
||||||
],
|
|
||||||
autofocus: props.autoFocus,
|
|
||||||
content: props.initialContent || null
|
|
||||||
}))
|
|
||||||
const ed = freshEditor()
|
|
||||||
ed && setEditor(ed)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
createReaction(
|
if (props.onChange) {
|
||||||
on(
|
createEffect(() => {
|
||||||
editor,
|
props.onChange?.(html() || '')
|
||||||
(e) => {
|
})
|
||||||
e?.commands.clearContent(props.resetToInitial || props.setClear)
|
}
|
||||||
props.initialContent && e?.commands.setContent(props.initialContent)
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
createEffect(
|
createEffect(() => {
|
||||||
on(
|
if (html()) {
|
||||||
html,
|
setCounter(editor()?.storage.characterCount.characters())
|
||||||
(content) => {
|
}
|
||||||
content && setCounter(editor()?.storage.characterCount.characters())
|
})
|
||||||
props.onChange?.(content || '')
|
|
||||||
},
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const maxHeightStyle = {
|
const maxHeightStyle = {
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
|
@ -272,7 +290,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
<Show when={props.label && counter() > 0}>
|
<Show when={props.label && counter() > 0}>
|
||||||
<div class={styles.label}>{props.label}</div>
|
<div class={styles.label}>{props.label}</div>
|
||||||
</Show>
|
</Show>
|
||||||
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={(el) => (editorEl = el)} />
|
<div style={props.maxHeight ? maxHeightStyle : undefined} ref={setEditorElement} />
|
||||||
<Show when={!props.onlyBubbleControls}>
|
<Show when={!props.onlyBubbleControls}>
|
||||||
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
|
<div class={clsx(styles.controls, { [styles.alwaysVisible]: props.controlsAlwaysVisible })}>
|
||||||
<div class={styles.actions}>
|
<div class={styles.actions}>
|
||||||
|
@ -343,7 +361,7 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<Show when={!props.onChange}>
|
<Show when={!props.onChange}>
|
||||||
<div class={styles.buttons}>
|
<div class={styles.buttons}>
|
||||||
<Show when={props.isCancelButtonVisible}>
|
<Show when={isCancelButtonVisible()}>
|
||||||
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.isPosting} fallback={<Loading />}>
|
<Show when={!props.isPosting} fallback={<Loading />}>
|
||||||
|
@ -387,4 +405,4 @@ const SimplifiedEditor = (props: Props) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SimplifiedEditor
|
export default SimplifiedEditor // "export default" need to use for asynchronous (lazy) imports in the comments tree
|
||||||
|
|
|
@ -10,7 +10,7 @@ declare module '@tiptap/core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Node.create({
|
export const ArticleNode = Node.create({
|
||||||
name: 'article',
|
name: 'article',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
content: 'block+',
|
content: 'block+',
|
||||||
|
@ -65,3 +65,5 @@ export default Node.create({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export default ArticleNode
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { useNavigate } from '@solidjs/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createMemo, createSignal, lazy, onMount } from 'solid-js'
|
import { Show, createEffect, createMemo, createSignal, lazy, onMount } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { Button } from '~/components/_shared/Button'
|
import { Button } from '~/components/_shared/Button'
|
||||||
import { Icon } from '~/components/_shared/Icon'
|
import { Icon } from '~/components/_shared/Icon'
|
||||||
import { Image } from '~/components/_shared/Image'
|
import { Image } from '~/components/_shared/Image'
|
||||||
|
@ -11,11 +11,10 @@ import { useSession } from '~/context/session'
|
||||||
import { useTopics } from '~/context/topics'
|
import { useTopics } from '~/context/topics'
|
||||||
import { useSnackbar, useUI } from '~/context/ui'
|
import { useSnackbar, useUI } from '~/context/ui'
|
||||||
import { Topic } from '~/graphql/schema/core.gen'
|
import { Topic } from '~/graphql/schema/core.gen'
|
||||||
|
import { UploadedFile } from '~/types/upload'
|
||||||
import { TopicSelect, UploadModalContent } from '../../Editor'
|
import { TopicSelect, UploadModalContent } from '../../Editor'
|
||||||
import { Modal } from '../../_shared/Modal'
|
import { Modal } from '../../_shared/Modal'
|
||||||
|
|
||||||
import { useNavigate } from '@solidjs/router'
|
|
||||||
import { UploadedFile } from '~/types/upload'
|
|
||||||
import stylesBeside from '../../Feed/Beside.module.scss'
|
import stylesBeside from '../../Feed/Beside.module.scss'
|
||||||
import styles from './PublishSettings.module.scss'
|
import styles from './PublishSettings.module.scss'
|
||||||
|
|
||||||
|
|
|
@ -73,9 +73,10 @@ export const AuthorsProvider = (props: { children: JSX.Element }) => {
|
||||||
console.debug('[context.authors] storing new authors:', newAuthors)
|
console.debug('[context.authors] storing new authors:', newAuthors)
|
||||||
setAuthors((prevAuthors) => {
|
setAuthors((prevAuthors) => {
|
||||||
const updatedAuthors = { ...prevAuthors }
|
const updatedAuthors = { ...prevAuthors }
|
||||||
newAuthors.forEach((author) => {
|
Array.isArray(newAuthors) &&
|
||||||
updatedAuthors[author.slug] = author
|
newAuthors.forEach((author) => {
|
||||||
})
|
updatedAuthors[author.slug] = author
|
||||||
|
})
|
||||||
return updatedAuthors
|
return updatedAuthors
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
51
vite.config.ts
Normal file
51
vite.config.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { CSSOptions } from 'vite'
|
||||||
|
// import { visualizer } from 'rollup-plugin-visualizer'
|
||||||
|
import mkcert from 'vite-plugin-mkcert'
|
||||||
|
import { PolyfillOptions, nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
import sassDts from 'vite-plugin-sass-dts'
|
||||||
|
|
||||||
|
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] build for ${runtime}!`)
|
||||||
|
|
||||||
|
const polyfillOptions = {
|
||||||
|
include: ['path', 'stream', 'util'],
|
||||||
|
exclude: ['http'],
|
||||||
|
globals: {
|
||||||
|
Buffer: true
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
fs: 'memfs'
|
||||||
|
},
|
||||||
|
protocolImports: true
|
||||||
|
} as PolyfillOptions
|
||||||
|
|
||||||
|
export default {
|
||||||
|
envPrefix: 'PUBLIC_',
|
||||||
|
plugins: [!isVercel && mkcert(), nodePolyfills(polyfillOptions), sassDts()],
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: '@import "src/styles/imports";\n',
|
||||||
|
includePaths: ['./public', './src/styles']
|
||||||
|
}
|
||||||
|
} as CSSOptions['preprocessorOptions']
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'esnext',
|
||||||
|
sourcemap: true,
|
||||||
|
rollupOptions: {
|
||||||
|
// plugins: [visualizer()]
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
icons: ['./src/components/_shared/Icon/Icon.tsx'],
|
||||||
|
session: ['./src/context/session.tsx'],
|
||||||
|
editor: ['./src/context/editor.tsx'],
|
||||||
|
connect: ['./src/context/connect.tsx']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user