abc-sort-fix

This commit is contained in:
Untone 2024-09-16 02:41:48 +03:00
parent 433a74a58a
commit 6e0a830168
18 changed files with 442 additions and 261 deletions

View File

@ -158,11 +158,11 @@ export const CommentsTree = (props: Props) => {
<SimplifiedEditor
quoteEnabled={true}
imageEnabled={true}
options={{ autofocus: false }}
autoFocus={false}
submitByCtrlEnter={true}
placeholder={t('Write a comment...')}
onSubmit={(value) => handleSubmitComment(value)}
reset={clearEditor()}
setClear={clearEditor()}
isPosting={posting()}
/>
</ShowIfAuthenticated>

View File

@ -0,0 +1,162 @@
import { Editor } 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 } from './Editor'
// Mock any necessary 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<ShoutForm>({
body: '',
slug: '',
shoutId: 0,
title: '',
selectedTopics: []
})
const [_formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
const [editor, setEditor] = createSignal<Editor | undefined>()
const mockEditorContext: EditorContextType = {
countWords: () => 0,
isEditorPanelVisible: (): boolean => {
throw new Error('Function not implemented.')
},
wordCounter: (): { characters: number; words: number } => {
throw new Error('Function not implemented.')
},
form: {
layout: undefined,
shoutId: 0,
slug: '',
title: '',
subtitle: undefined,
lead: undefined,
description: undefined,
selectedTopics: [],
mainTopic: undefined,
body: '',
coverImageUrl: undefined,
media: undefined
},
formErrors: {} as Record<keyof ShoutForm, string>,
setEditor,
editor,
saveShout: (_form: ShoutForm): Promise<void> => {
throw new Error('Function not implemented.')
},
saveDraft: (_form: ShoutForm): Promise<void> => {
throw new Error('Function not implemented.')
},
saveDraftToLocalStorage: (_form: ShoutForm): void => {
throw new Error('Function not implemented.')
},
getDraftFromLocalStorage: (_shoutId: number): ShoutForm => {
throw new Error('Function not implemented.')
},
publishShout: (_form: ShoutForm): Promise<void> => {
throw new Error('Function not implemented.')
},
publishShoutById: (_shoutId: number): Promise<void> => {
throw new Error('Function not implemented.')
},
deleteShout: (_shoutId: number): Promise<boolean> => {
throw new Error('Function not implemented.')
},
toggleEditorPanel: (): void => {
throw new Error('Function not implemented.')
},
setForm,
setFormErrors
}
const mockSnackbarContext = {
showSnackbar: console.log
}
const meta: Meta<typeof EditorComponent> = {
title: 'Components/Editor',
component: EditorComponent,
argTypes: {
shoutId: {
control: 'number',
description: 'Unique identifier for the shout (document)',
defaultValue: 1
},
initialContent: {
control: 'text',
description: 'Initial content for the editor',
defaultValue: ''
},
onChange: {
action: 'contentChanged',
description: 'Callback when the content changes'
},
disableCollaboration: {
control: 'boolean',
description: 'Disable collaboration features for Storybook',
defaultValue: true
}
}
}
export default meta
type Story = StoryObj<typeof EditorComponent>
export const Default: Story = {
render: (args) => {
const [_content, setContent] = createSignal(args.initialContent || '')
return (
<SessionContext.Provider value={mockSession as SessionContextType}>
<LocalizeContext.Provider value={mockLocalize as LocalizeContextType}>
<SnackbarContext.Provider value={mockSnackbarContext as SnackbarContextType}>
<EditorContext.Provider value={mockEditorContext as EditorContextType}>
<EditorComponent
{...args}
onChange={(text: string) => {
args.onChange(text)
setContent(text)
}}
/>
</EditorContext.Provider>
</SnackbarContext.Provider>
</LocalizeContext.Provider>
</SessionContext.Provider>
)
},
args: {
shoutId: 1,
initialContent: '',
disableCollaboration: true
}
}
export const WithInitialContent: Story = {
...Default,
args: {
...Default.args,
initialContent: '<p>This is some initial content in the editor.</p>'
}
}

View File

@ -1,5 +1,5 @@
import { HocuspocusProvider } from '@hocuspocus/provider'
import { Editor, isTextSelection } from '@tiptap/core'
import { Editor, EditorOptions, isTextSelection } from '@tiptap/core'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { BulletList } from '@tiptap/extension-bullet-list'
@ -26,7 +26,7 @@ import { Strike } from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
import { Underline } from '@tiptap/extension-underline'
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import { createTiptapEditor } from 'solid-tiptap'
import uniqolor from 'uniqolor'
import { Doc } from 'yjs'
import { useEditorContext } from '~/context/editor'
@ -47,14 +47,15 @@ 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 { renderUploadedImage } from './renderUploadedImage'
type Props = {
shoutId: number
initialContent?: string
onChange: (text: string) => void
disableCollaboration?: boolean
}
const allowedImageTypes = new Set([
@ -78,28 +79,14 @@ export const EditorComponent = (props: Props) => {
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const { showSnackbar } = useSnackbar()
const docName = `shout-${props.shoutId}`
if (!yDocs[docName]) {
yDocs[docName] = new Doc()
}
if (!providers[docName]) {
providers[docName] = new HocuspocusProvider({
url: 'wss://hocuspocus.discours.io',
name: docName,
document: yDocs[docName],
token: session()?.access_token || ''
})
}
const [editorElRef, setEditorElRef] = createSignal<HTMLElement>()
let textBubbleMenuRef: HTMLDivElement | undefined
let incutBubbleMenuRef: HTMLElement | undefined
let figureBubbleMenuRef: HTMLElement | undefined
let blockquoteBubbleMenuRef: HTMLElement | undefined
let floatingMenuRef: HTMLDivElement | undefined
const { setEditor, countWords } = useEditorContext()
const [extensions, setExtensions] = createSignal<EditorOptions['extensions']>([])
const [editorElRef, setEditorElRef] = createSignal<HTMLElement | undefined>()
const [textBubbleMenuRef, setTextBubbleMenuRef] = createSignal<HTMLDivElement | undefined>()
const [incutBubbleMenuRef, setIncutBubbleMenuRef] = createSignal<HTMLElement | undefined>()
const [figureBubbleMenuRef, setFigureBubbleMenuRef] = createSignal<HTMLElement | undefined>()
const [blockquoteBubbleMenuRef, setBlockquoteBubbleMenuRef] = createSignal<HTMLElement | undefined>()
const [floatingMenuRef, setFloatingMenuRef] = createSignal<HTMLDivElement | undefined>()
const handleClipboardPaste = async () => {
try {
@ -130,13 +117,8 @@ export const EditorComponent = (props: Props) => {
}
}
const { initialContent } = props
const { editor, setEditor, countWords } = useEditorContext()
createEffect(
on(editorElRef, (ee: HTMLElement | undefined) => {
if (ee) {
const freshEditor = createTiptapEditor<HTMLElement>(() => ({
element: ee,
const editor = createTiptapEditor(() => ({
element: editorElRef()!,
editorProps: {
attributes: {
class: 'articleEditor'
@ -149,57 +131,63 @@ export const EditorComponent = (props: Props) => {
return false
}
},
extensions: [
extensions: extensions(),
onTransaction: ({ transaction, editor }) => {
if (transaction.docChanged) {
// Get the current HTML content from the editor
const html = editor.getHTML()
// Trigger the onChange callback with the updated HTML
html && props.onChange(html)
// Get the word count from the editor's storage (using CharacterCount)
const wordCount = editor.storage.characterCount.words()
// Update the word count
wordCount && countWords(wordCount)
}
},
content: props.initialContent || ''
}))
createEffect(() => editor() && setEditor(editor() as Editor))
createEffect(
on(
[extensions, editorElRef, author, () => `shout-${props.shoutId}`],
([eee, element, a, docName]) =>
eee.length === 0 &&
a &&
element &&
setExtensions([
Document,
Text,
Paragraph,
Dropcursor,
CustomBlockquote,
Bold,
Italic,
Span,
ToggleTextWrap,
Strike,
HorizontalRule.configure({
HTMLAttributes: {
class: 'horizontalRule'
}
}),
Underline,
Link.extend({
inclusive: false
}).configure({
autolink: true,
openOnClick: false
}),
Heading.configure({
levels: [2, 3, 4]
}),
Heading.configure({ levels: [2, 3, 4] }),
BulletList,
OrderedList,
ListItem,
Collaboration.configure({
document: yDocs[docName]
}),
HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }),
Dropcursor,
CustomBlockquote,
Span,
ToggleTextWrap,
Underline,
Link.extend({ inclusive: false }).configure({ autolink: true, openOnClick: false }),
Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({
provider: providers[docName],
user: {
name: author().name,
color: uniqolor(author().slug).color
}
}),
Placeholder.configure({
placeholder: t('Add a link or click plus to embed media')
user: { name: a.name, color: uniqolor(a.slug).color }
}),
Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }),
Focus,
Gapcursor,
HardBreak,
Highlight.configure({
multicolor: true,
HTMLAttributes: {
class: 'highlight'
}
}),
Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }),
Image,
Iframe,
Figure,
@ -209,96 +197,107 @@ export const EditorComponent = (props: Props) => {
CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689
BubbleMenu.configure({
pluginKey: 'textBubbleMenu',
element: textBubbleMenuRef,
shouldShow: ({ editor: e, view, state, from, to }) => {
const { doc, selection } = state
const { empty } = selection
const isEmptyTextBlock =
doc.textBetween(from, to).length === 0 && isTextSelection(selection)
if (isEmptyTextBlock) {
element: textBubbleMenuRef(),
shouldShow: ({ editor: e, view, state: { doc, selection }, from, to }) => {
const isEmptyTextBlock = doc.textBetween(from, to).length === 0 && isTextSelection(selection)
isEmptyTextBlock &&
e?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
}
setIsCommonMarkup(e?.isActive('figcaption'))
const result =
(view.hasFocus() &&
!empty &&
!selection.empty &&
!isEmptyTextBlock &&
!e.isActive('image') &&
!e.isActive('figure')) ||
e.isActive('footnote') ||
(e.isActive('figcaption') && !empty)
(e.isActive('figcaption') && !selection.empty)
setShouldShowTextBubbleMenu(result)
return result
},
tippyOptions: {
onHide: () => {
const fe = freshEditor() as Editor
fe?.commands.focus()
}
onHide: () => editor()?.commands.focus() as false
}
}),
BubbleMenu.configure({
pluginKey: 'blockquoteBubbleMenu',
element: blockquoteBubbleMenuRef,
shouldShow: ({ editor: e, view, state }) => {
const { empty } = state.selection
return view.hasFocus() && !empty && e.isActive('blockquote')
}
element: blockquoteBubbleMenuRef(),
shouldShow: ({ editor: e, view, state }) =>
view.hasFocus() && !state.selection.empty && e.isActive('blockquote')
}),
BubbleMenu.configure({
pluginKey: 'figureBubbleMenu',
element: figureBubbleMenuRef,
shouldShow: ({ editor: e, view, state }) => {
const { empty } = state.selection
return view.hasFocus() && !empty && e.isActive('figure')
}
element: figureBubbleMenuRef(),
shouldShow: ({ editor: e, view, state }) =>
view.hasFocus() && !state.selection.empty && e.isActive('figure')
}),
BubbleMenu.configure({
pluginKey: 'incutBubbleMenu',
element: incutBubbleMenuRef,
shouldShow: ({ editor: e, view, state }) => {
const { empty } = state.selection
return view.hasFocus() && !empty && e.isActive('figcaption')
}
element: incutBubbleMenuRef(),
shouldShow: ({ editor: e, view, state }) =>
view.hasFocus() && !state.selection.empty && e.isActive('figcaption')
}),
FloatingMenu.configure({
element: floatingMenuRef,
element: floatingMenuRef(),
pluginKey: 'floatingMenu',
shouldShow: ({ editor: e, state }) => {
const { $anchor, empty } = state.selection
const isRootDepth = $anchor.depth === 1
if (!(isRootDepth && empty)) return false
shouldShow: ({ editor: e, state: { selection } }) => {
const isRootDepth = selection.$anchor.depth === 1
if (!(isRootDepth && selection.empty)) return false
return !(e.isActive('codeBlock') || e.isActive('heading'))
}
}),
TrailingNode,
ArticleNode
],
onTransaction: ({ transaction }) => {
if (transaction.docChanged) {
const fe = freshEditor()
if (fe) {
const changeHandle = useEditorHTML(() => fe as Editor | undefined)
props.onChange(changeHandle() || '')
countWords(fe?.storage.characterCount.words())
}
}
},
content: initialContent
}))
])
)
)
if (freshEditor) {
editorElRef()?.addEventListener('focus', (_event) => {
if (freshEditor()?.isActive('figcaption')) {
freshEditor()?.commands.focus()
createEffect(
on(
[
() => !props.disableCollaboration,
() => `shout-${props.shoutId}`,
() => session()?.access_token || '',
author
],
([collab, docName, token, profile]) => {
if (!yDocs[docName]) {
yDocs[docName] = new Doc()
}
if (!providers[docName]) {
providers[docName] = new HocuspocusProvider({
url: 'wss://hocuspocus.discours.io',
name: docName,
document: yDocs[docName],
token
})
}
collab &&
setExtensions((old: EditorOptions['extensions']) => [
...old,
Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({
provider: providers[docName],
user: {
name: profile.name,
color: uniqolor(profile.slug).color
}
})
setEditor(freshEditor() as Editor)
])
}
)
)
createEffect(
on(editorElRef, (ee: HTMLElement | undefined) => {
ee?.addEventListener('focus', (_event) => {
if (editor()?.isActive('figcaption')) {
editor()?.commands.focus()
}
})
})
)
onCleanup(() => {
@ -318,27 +317,12 @@ export const EditorComponent = (props: Props) => {
shouldShow={shouldShowTextBubbleMenu()}
isCommonMarkup={isCommonMarkup()}
editor={editor() as Editor}
ref={(el) => (textBubbleMenuRef = el)}
ref={setTextBubbleMenuRef}
/>
<BlockquoteBubbleMenu
ref={(el) => {
blockquoteBubbleMenuRef = el
}}
editor={editor() as Editor}
/>
<FigureBubbleMenu
editor={editor() as Editor}
ref={(el) => {
figureBubbleMenuRef = el
}}
/>
<IncutBubbleMenu
editor={editor() as Editor}
ref={(el) => {
incutBubbleMenuRef = el
}}
/>
<EditorFloatingMenu editor={editor() as Editor} ref={(el) => (floatingMenuRef = el)} />
<BlockquoteBubbleMenu ref={setBlockquoteBubbleMenuRef} editor={editor() as Editor} />
<FigureBubbleMenu editor={editor() as Editor} ref={setFigureBubbleMenuRef} />
<IncutBubbleMenu editor={editor() as Editor} ref={setIncutBubbleMenuRef} />
<EditorFloatingMenu editor={editor() as Editor} ref={setFloatingMenuRef} />
</Show>
</>
)

View File

@ -25,12 +25,12 @@ export const Panel = (props: Props) => {
const {
isEditorPanelVisible,
wordCounter,
editor,
form,
toggleEditorPanel,
saveShout,
saveDraft,
publishShout
publishShout,
editor
} = useEditorContext()
let containerRef: HTMLElement | undefined
@ -58,7 +58,7 @@ export const Panel = (props: Props) => {
}
}
const html = useEditorHTML(() => editor()) // FIXME: lost current() call
const html = useEditorHTML(editor)
const handleFixTypographyClick = () => {
editor()?.commands.setContent(typograf.execute(html() || '')) // here too

View File

@ -1,3 +1,4 @@
import { Editor } from '@tiptap/core'
import { Blockquote } from '@tiptap/extension-blockquote'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { CharacterCount } from '@tiptap/extension-character-count'
@ -14,12 +15,14 @@ import {
useEditorIsEmpty,
useEditorIsFocused
} from 'solid-tiptap'
import { useLocalize } from '~/context/localize'
import { useUI } from '~/context/ui'
import { base } from '~/lib/editorOptions'
import { UploadedFile } from '~/types/upload'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { Loading } from '../_shared/Loading'
import { Modal } from '../_shared/Modal/Modal'
import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { LinkBubbleMenuModule } from './LinkBubbleMenu'
@ -27,14 +30,10 @@ import { TextBubbleMenu } from './TextBubbleMenu'
import { UploadModalContent } from './UploadModalContent'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { Editor } from '@tiptap/core'
import { useUI } from '~/context/ui'
import { base } from '~/lib/editorOptions'
import { Modal } from '../_shared/Modal/Modal'
import styles from './SimplifiedEditor.module.scss'
import { renderUploadedImage } from './renderUploadedImage'
import styles from './SimplifiedEditor.module.scss'
type Props = {
placeholder: string
initialContent?: string

View File

@ -5,7 +5,7 @@ import { Icon } from '~/components/_shared/Icon'
import { useLocalize } from '~/context/localize'
import { useTopics } from '~/context/topics'
import type { Topic } from '~/graphql/schema/core.gen'
import { ruChars } from '~/intl/chars'
import { notLatin } from '~/intl/chars'
import { getRandomItemsFromArray } from '~/utils/random'
import styles from './TopicsNav.module.scss'
@ -13,7 +13,7 @@ export const RandomTopics = () => {
const { sortedTopics } = useTopics()
const { lang, t } = useLocalize()
const tag = (topic: Topic) =>
ruChars.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
notLatin.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
createEffect(
on(sortedTopics, (ttt: Topic[]) => {

View File

@ -11,7 +11,6 @@ import { useLocalize } from '~/context/localize'
import type { Author } from '~/graphql/schema/core.gen'
import { dummyFilter } from '~/intl/dummyFilter'
import { authorLetterReduce, translateAuthor } from '~/intl/translate'
// import { byFirstChar, byStat } from '~/lib/sort'
import { scrollHandler } from '~/utils/scroll'
import styles from './AllAuthors.module.scss'
import stylesAuthorList from './AuthorsList.module.scss'
@ -25,8 +24,8 @@ type Props = {
export const AUTHORS_PER_PAGE = 20
export const ABC = {
ru: 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ#',
en: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'
ru: 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@',
en: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ@'
}
// useAuthors sorted from context, set filter/sort

View File

@ -6,8 +6,9 @@ import { SearchField } from '~/components/_shared/SearchField'
import { useLocalize } from '~/context/localize'
import { useTopics } from '~/context/topics'
import type { Topic } from '~/graphql/schema/core.gen'
import { enChars, ruChars } from '~/intl/chars'
import { findFirstReadableCharIndex, notLatin, notRus } from '~/intl/chars'
import { dummyFilter } from '~/intl/dummyFilter'
import { capitalize } from '~/utils/capitalize'
import { scrollHandler } from '~/utils/scroll'
import { TopicBadge } from '../../Topic/TopicBadge'
import styles from './AllTopics.module.scss'
@ -28,16 +29,20 @@ export const AllTopics = (props: Props) => {
const { setTopicsSort, sortedTopics } = useTopics()
const topics = createMemo(() => sortedTopics() || props.topics)
const [searchParams, changeSearchParams] = useSearchParams<{ by?: string }>()
onMount(() => changeSearchParams({ by: 'shouts' }))
createEffect(on(() => searchParams?.by || 'shouts', setTopicsSort, { defer: true }))
onMount(() => !searchParams?.by && changeSearchParams({ by: 'shouts' }))
// sorted derivative
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
return topics().reduce(
(acc, topic) => {
let letter = lang() === 'en' ? topic.slug[0].toUpperCase() : (topic?.title?.[0] || '').toUpperCase()
if (enChars.test(letter) && lang() === 'ru') letter = '#'
if (ruChars.test(letter) && lang() === 'en') letter = '#'
const firstCharIndex = findFirstReadableCharIndex(topic?.title || '')
let letter =
lang() === 'en'
? topic.slug[0].toUpperCase()
: (topic?.title?.[firstCharIndex] || '').toUpperCase()
if (notRus.test(letter) && lang() === 'ru') letter = '#'
if (notLatin.test(letter) && lang() === 'en') letter = '#'
if (!acc[letter]) acc[letter] = []
acc[letter].push(topic)
return acc
@ -124,7 +129,9 @@ export const AllTopics = (props: Props) => {
<For each={byLetter()[letter]}>
{(topic) => (
<div class={clsx(styles.topicTitle, 'col-sm-12 col-md-8')}>
<A href={`/topic/${topic.slug}`}>{topic.title || topic.slug}</A>
<A href={`/topic/${topic.slug}`}>
{lang() !== 'ru' ? capitalize(topic.slug.replaceAll('-', ' ')) : topic.title}
</A>
<Show when={topic.stat?.shouts || 0}>
<span class={styles.articlesCounter}>{topic.stat?.shouts || 0}</span>
</Show>

View File

@ -46,7 +46,6 @@ export const EditSettingsView = (props: Props) => {
const [isScrolled, setIsScrolled] = createSignal(false)
const { session } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const { form, setForm, saveDraft, saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext()
const [shoutTopics, setShoutTopics] = createSignal<Topic[]>([])
const [draft, setDraft] = createSignal()

View File

@ -1,3 +1,4 @@
import { Editor } from '@tiptap/core'
import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal'
import {
@ -32,7 +33,7 @@ import { isDesktop } from '~/lib/mediaQuery'
import { LayoutType } from '~/types/common'
import { MediaItem } from '~/types/mediaitem'
import { clone } from '~/utils/clone'
import { Editor, Panel } from '../../Editor'
import { Editor as EditorComponent, Panel } from '../../Editor'
import { AudioUploader } from '../../Editor/AudioUploader'
import { AutoSaveNotice } from '../../Editor/AutoSaveNotice'
import { VideoUploader } from '../../Editor/VideoUploader'
@ -45,6 +46,7 @@ const GrowingTextarea = lazy(() => import('~/components/_shared/GrowingTextarea/
type Props = {
shout: Shout
editor: Editor
}
export const MAX_HEADER_LIMIT = 100
@ -456,10 +458,10 @@ export const EditView = (props: Props) => {
</div>
</div>
<Show when={form?.shoutId} fallback={<Loading />}>
<Editor
<EditorComponent
shoutId={form.shoutId}
initialContent={form.body}
onChange={(body) => handleInputChange('body', body)}
onChange={(body: string) => handleInputChange('body', body)}
/>
</Show>
</div>

View File

@ -1,4 +1,5 @@
import { useMatch, useNavigate } from '@solidjs/router'
import { Editor } from '@tiptap/core'
import type { JSX } from 'solid-js'
import { Accessor, createContext, createMemo, createSignal, useContext } from 'solid-js'
import { SetStoreFunction, createStore } from 'solid-js/store'
@ -33,7 +34,7 @@ export type ShoutForm = {
media?: string
}
type EditorContextType = {
export type EditorContextType = {
isEditorPanelVisible: Accessor<boolean>
wordCounter: Accessor<WordCounter>
form: ShoutForm
@ -49,9 +50,11 @@ type EditorContextType = {
countWords: (value: WordCounter) => void
setForm: SetStoreFunction<ShoutForm>
setFormErrors: SetStoreFunction<Record<keyof ShoutForm, string>>
editor: Accessor<Editor | undefined>
setEditor: (e: Editor) => void
}
const EditorContext = createContext<EditorContextType>({} as EditorContextType)
export const EditorContext = createContext<EditorContextType>({} as EditorContextType)
export function useEditorContext() {
return useContext(EditorContext)
@ -83,7 +86,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
const matchEditSettings = useMatch(() => '/editSettings')
const { session } = useSession()
const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token))
const [editor, setEditor] = createSignal<Editor | undefined>()
const { addFeed } = useFeed()
const snackbar = useSnackbar()
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
@ -278,7 +281,9 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
toggleEditorPanel,
countWords,
setForm,
setFormErrors
setFormErrors,
editor,
setEditor
}
const value: EditorContextType = {

View File

@ -15,7 +15,7 @@ import { processPrepositions } from '~/intl/prepositions'
i18nextInit()
type LocalizeContextType = {
export type LocalizeContextType = {
t: i18n['t']
lang: Accessor<Language>
setLang: (lang: Language) => void
@ -26,7 +26,7 @@ type LocalizeContextType = {
export type Language = 'ru' | 'en'
const LocalizeContext = createContext<LocalizeContextType>({
export const LocalizeContext = createContext<LocalizeContextType>({
t: (s: string) => s
} as LocalizeContextType)

View File

@ -85,7 +85,8 @@ const metaRes = {
}
}
}
const SessionContext = createContext<SessionContextType>({} as SessionContextType)
export const SessionContext = createContext<SessionContextType>({} as SessionContextType)
export function useSession() {
return useContext(SessionContext)

View File

@ -17,12 +17,12 @@ type SnackbarMessage = {
duration?: number
}
type SnackbarContextType = {
export type SnackbarContextType = {
snackbarMessage: Accessor<SnackbarMessage | null | undefined>
showSnackbar: (message: SnackbarMessage) => Promise<void>
}
const SnackbarContext = createContext<SnackbarContextType>({
export const SnackbarContext = createContext<SnackbarContextType>({
snackbarMessage: () => undefined,
showSnackbar: async (_m: SnackbarMessage) => undefined
} as SnackbarContextType)

View File

@ -1,6 +1,16 @@
export const allChars = /[^\dA-zА-я]/
export const slugChars = /[^\da-z]/g
export const enChars = /[^A-z]/
export const ruChars = /[^ËА-яё]/
export const notChar = /[^\dA-Za-zА-Яа-я]/
export const allChar = /[\dA-Za-zА-Яа-я]/
export const notLatin = /[^A-Za-z]/
export const notRus = /[^ËА-Яа-яё]/
export const sentenceSeparator = /{!|\?|:|;}\s/
export const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
export function findFirstReadableCharIndex(input: string): number {
for (let i = 0; i < input.length; i++) {
// Test each character against the "allChar" regex (readable characters).
if (allChar.test(input[i])) {
return i // Return the index of the first non-readable character
}
}
return 0 // Return -1 if no non-readable characters are found
}

View File

@ -1,6 +1,6 @@
import { Author } from '~/graphql/schema/core.gen'
import { capitalize } from '~/utils/capitalize'
import { allChars, cyrillicRegex, enChars, ruChars } from './chars'
import { cyrillicRegex, findFirstReadableCharIndex, notChar, notLatin, notRus } from './chars'
import { translit } from './translit'
export const isCyrillic = (s: string): boolean => {
@ -17,22 +17,35 @@ export const translateAuthor = (author: Author, lng: string) =>
export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Author, lng: string) => {
let letter = ''
if (!letter && author && author.name) {
const name =
translateAuthor(author, lng || 'ru')
?.replace(allChars, ' ')
.trim() || ''
const nameParts = name.trim().split(' ')
const found = nameParts.filter(Boolean).pop()
if (found && found.length > 0) {
letter = found[0].toUpperCase()
}
}
if (ruChars.test(letter) && lng === 'ru') letter = '@'
if (enChars.test(letter) && lng === 'en') letter = '@'
if (author?.name) {
// Get the translated author name and clean it up
const name = translateAuthor(author, lng || 'ru')?.trim() || ''
const nameParts = name.split(' ')
const lastName = nameParts.filter(Boolean).pop()?.replace(notChar, ' ').trim() || '' // Replace non-readable characters
// Get the last part of the name
if (lastName && lastName.length > 0) {
const firstCharIndex = findFirstReadableCharIndex(lastName)
// Make sure the index is valid before accessing the character
if (firstCharIndex !== -1) {
letter = lastName[firstCharIndex].toUpperCase()
}
}
}
// Handle non-readable letters based on the language
if (notRus.test(letter) && lng === 'ru') letter = '@'
if (notLatin.test(letter) && lng === 'en') letter = '@'
// Initialize the letter group if it doesn't exist
if (!acc[letter]) acc[letter] = []
// Translate the author's name for the current language
author.name = translateAuthor(author, lng)
// Push the author into the corresponding letter group
acc[letter].push(author)
// Sort authors within each letter group alphabetically by name

View File

@ -1,5 +1,5 @@
import translitConfig from './abc-translit.json'
import { ruChars, slugChars } from './chars'
import { notChar, notLatin } from './chars'
const ru2en: { [key: string]: string } = translitConfig
@ -8,7 +8,7 @@ export const translit = (str: string) => {
return ''
}
const isCyrillic = ruChars.test(str)
const isCyrillic = notLatin.test(str)
if (!isCyrillic) {
return str
@ -18,5 +18,5 @@ export const translit = (str: string) => {
}
export const slugify = (text: string) => {
return translit(text.toLowerCase()).replaceAll(' ', '-').replaceAll(slugChars, '')
return translit(text.toLowerCase()).replaceAll(' ', '-').replaceAll(notChar, '')
}

View File

@ -24,7 +24,7 @@ const polyfillOptions = {
export default {
resolve: {
alias: {
'~': path.resolve(__dirname, './src')
'~': path.resolve('./src')
}
},
envPrefix: 'PUBLIC_',