diff --git a/.gitignore b/.gitignore index 1854c000..881888d4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,9 +25,9 @@ bun.lockb /plawright-report/ target .github/dependabot.yml - .output .vinxi *.pem edge.* .vscode/settings.json +storybook-static diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index 91a12ca8..7e913f5f 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -1,3 +1,4 @@ +import type { Page } from '@playwright/test' import type { TestRunnerConfig } from '@storybook/test-runner' import { checkA11y, injectAxe } from 'axe-playwright' @@ -5,11 +6,11 @@ import { checkA11y, injectAxe } from 'axe-playwright' * See https://storybook.js.org/docs/react/writing-tests/test-runner#test-hook-api-experimental * to learn more about the test-runner hooks API. */ -const a11yConfig: TestRunnerConfig = { - async preRender(page) { +const a11yConfig = { + async preRender(page: Page) { await injectAxe(page) }, - async postRender(page) { + async postRender(page: Page) { await checkA11y(page, '#storybook-root', { detailedReport: true, detailedReportOptions: { @@ -17,6 +18,6 @@ const a11yConfig: TestRunnerConfig = { } }) } -} +} as TestRunnerConfig module.exports = a11yConfig diff --git a/.stylelintignore b/.stylelintignore index fd2fbae2..f313728d 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,2 +1,6 @@ -.vercel/ +node_modules dist/ +storybook-static +.output +.vinxi +.vercel diff --git a/app.config.ts b/app.config.ts index d51d23b1..7b29f3ae 100644 --- a/app.config.ts +++ b/app.config.ts @@ -1,16 +1,12 @@ import { SolidStartInlineConfig, defineConfig } from '@solidjs/start/config' -import dotenv from 'dotenv' -import viteConfig from './vite.config' - -// Load environment variables from .env file -dotenv.config() +import viteConfig, { isDev } from './vite.config' const isVercel = Boolean(process.env.VERCEL) const isNetlify = Boolean(process.env.NETLIFY) const isBun = Boolean(process.env.BUN) -export const runtime = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node' -console.info(`[app.config] solid-start build for {> ${runtime} <}`) +const preset = isNetlify ? 'netlify' : isVercel ? 'vercel_edge' : isBun ? 'bun' : 'node' +console.info(`[app.config] solid-start preset {> ${preset} <}`) export default defineConfig({ nitro: { @@ -18,10 +14,10 @@ export default defineConfig({ }, ssr: true, server: { - preset: runtime, + preset, port: 3000, https: true }, - devOverlay: true, + devOverlay: isDev, vite: viteConfig } as SolidStartInlineConfig) diff --git a/package.json b/package.json index c34f9db0..09e86c9b 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "@tiptap/starter-kit": "^2.7.2", "@types/cookie": "^0.6.0", "@types/cookie-signature": "^1.1.2", - "@types/node": "^22.6.0", + "@types/node": "^22.6.1", "@types/throttle-debounce": "^5.0.2", "@urql/core": "^5.0.6", "axe-playwright": "^2.0.2", diff --git a/src/components/Editor/Editor.tsx b/src/components/Editor/Editor.tsx index 81d31163..04879023 100644 --- a/src/components/Editor/Editor.tsx +++ b/src/components/Editor/Editor.tsx @@ -4,14 +4,7 @@ import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { CharacterCount } from '@tiptap/extension-character-count' import { Collaboration } from '@tiptap/extension-collaboration' import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor' -import { Dropcursor } from '@tiptap/extension-dropcursor' import { FloatingMenu } from '@tiptap/extension-floating-menu' -import Focus from '@tiptap/extension-focus' -import { Gapcursor } from '@tiptap/extension-gapcursor' -import { HardBreak } from '@tiptap/extension-hard-break' -import { Highlight } from '@tiptap/extension-highlight' -import { HorizontalRule } from '@tiptap/extension-horizontal-rule' -import { Image } from '@tiptap/extension-image' import { Placeholder } from '@tiptap/extension-placeholder' import { Show, createEffect, createMemo, createSignal, on, onCleanup } from 'solid-js' import uniqolor from 'uniqolor' @@ -21,23 +14,14 @@ import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' import { useSnackbar } from '~/context/ui' import { Author } from '~/graphql/schema/core.gen' +import { base, custom, extended } from '~/lib/editorExtensions' import { handleImageUpload } from '~/lib/handleImageUpload' import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu' import { EditorFloatingMenu } from './EditorFloatingMenu' import { TextBubbleMenu } from './TextBubbleMenu' -import { ArticleNode } from './extensions/Article' -import { CustomBlockquote } from './extensions/CustomBlockquote' -import { Figcaption } from './extensions/Figcaption' -import { Figure } from './extensions/Figure' -import { Footnote } from './extensions/Footnote' -import { Iframe } from './extensions/Iframe' -import { Span } from './extensions/Span' -import { ToggleTextWrap } from './extensions/ToggleTextWrap' -import { TrailingNode } from './extensions/TrailingNode' import { renderUploadedImage } from './renderUploadedImage' import './Prosemirror.scss' -import { base } from '~/lib/editorOptions' export type EditorComponentProps = { shoutId: number @@ -118,26 +102,11 @@ export const EditorComponent = (props: EditorComponentProps) => { }, extensions: [ ...base, + ...custom, + ...extended, - HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }), - Dropcursor, - CustomBlockquote, - Span, - ToggleTextWrap, Placeholder.configure({ placeholder: t('Add a link or click plus to embed media') }), - Focus, - Gapcursor, - HardBreak, - Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }), - Image, - Iframe, - Figure, - Figcaption, - Footnote, - ToggleTextWrap, CharacterCount.configure(), // https://github.com/ueberdosis/tiptap/issues/2589#issuecomment-1093084689 - TrailingNode, - ArticleNode, // menus diff --git a/src/components/Editor/MiniEditor/MiniEditor.tsx b/src/components/Editor/MiniEditor/MiniEditor.tsx index c866901f..011d951e 100644 --- a/src/components/Editor/MiniEditor/MiniEditor.tsx +++ b/src/components/Editor/MiniEditor/MiniEditor.tsx @@ -15,7 +15,7 @@ import { Icon } from '~/components/_shared/Icon/Icon' import { Popover } from '~/components/_shared/Popover/Popover' import { useLocalize } from '~/context/localize' import { useUI } from '~/context/ui' -import { base } from '~/lib/editorOptions' +import { base } from '~/lib/editorExtensions' import { InsertLinkForm } from '../InsertLinkForm/InsertLinkForm' import styles from '../SimplifiedEditor.module.scss' diff --git a/src/components/Editor/SimplifiedEditor.tsx b/src/components/Editor/SimplifiedEditor.tsx index 2f689753..a474f9c7 100644 --- a/src/components/Editor/SimplifiedEditor.tsx +++ b/src/components/Editor/SimplifiedEditor.tsx @@ -8,7 +8,7 @@ import { Portal } from 'solid-js/web' import { createEditorTransaction, useEditorHTML, useEditorIsEmpty, useEditorIsFocused } from 'solid-tiptap' import { useEditorContext } from '~/context/editor' import { useUI } from '~/context/ui' -import { base, custom } from '~/lib/editorOptions' +import { base, custom } from '~/lib/editorExtensions' import { useEscKeyDownHandler } from '~/lib/useEscKeyDownHandler' import { UploadedFile } from '~/types/upload' import { Modal } from '../_shared/Modal/Modal' diff --git a/src/components/Views/EditView/EditView.tsx b/src/components/Views/EditView/EditView.tsx index db2a4edd..cfb1b4d3 100644 --- a/src/components/Views/EditView/EditView.tsx +++ b/src/components/Views/EditView/EditView.tsx @@ -1,7 +1,6 @@ import { clsx } from 'clsx' import deepEqual from 'fast-deep-equal' import { - Accessor, Show, createEffect, createMemo, @@ -65,10 +64,7 @@ const handleScrollTopButtonClick = (ev: MouseEvent | TouchEvent) => { export const EditView = (props: Props) => { const { t } = useLocalize() - const [isScrolled, setIsScrolled] = createSignal(false) const { session } = useSession() - const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) - const { form, formErrors, @@ -78,14 +74,20 @@ export const EditView = (props: Props) => { saveDraftToLocalStorage, getDraftFromLocalStorage } = useEditorContext() - const [shoutTopics, setShoutTopics] = createSignal([]) - const [draft, setDraft] = createSignal() - let subtitleInput: HTMLTextAreaElement | null + + const [subtitleInput, setSubtitleInput] = createSignal() const [prevForm, setPrevForm] = createStore(clone(form)) const [saving, setSaving] = createSignal(false) const [isSubtitleVisible, setIsSubtitleVisible] = createSignal(Boolean(form.subtitle)) const [isLeadVisible, setIsLeadVisible] = createSignal(Boolean(form.lead)) - const mediaItems: Accessor = createMemo(() => JSON.parse(form.media || '[]')) + const [isScrolled, setIsScrolled] = createSignal(false) + const [shoutTopics, setShoutTopics] = createSignal([]) + const [draft, setDraft] = createSignal() + const [mediaItems, setMediaItems] = createSignal([]) + + const client = createMemo(() => graphqlClientCreate(coreApiUrl, session()?.access_token)) + + createEffect(() => setMediaItems(JSON.parse(form.media || '[]'))) createEffect( on( @@ -97,7 +99,7 @@ export const EditView = (props: Props) => { const stored = getDraftFromLocalStorage(shout.id) if (stored) { // console.info(`[EditView] got stored shout: ${stored}`) - setDraft(stored) + setDraft((old) => ({...old, ...stored} as Shout)) } else { if (!shout.slug) { console.warn(`[EditView] shout has no slug! ${shout}`) @@ -131,7 +133,7 @@ export const EditView = (props: Props) => { (d) => { if (d) { const draftForm = Object.keys(d) ? d : { shoutId: props.shout.id } - setForm(draftForm) + setForm(draftForm as ShoutForm) console.debug('draft from localstorage: ', draftForm) } }, @@ -267,7 +269,7 @@ export const EditView = (props: Props) => { const showSubtitleInput = () => { setIsSubtitleVisible(true) - subtitleInput?.focus() + subtitleInput()?.focus() } const showLeadInput = () => { @@ -359,7 +361,7 @@ export const EditView = (props: Props) => { (subtitleInput = el)} + textAreaRef={setSubtitleInput} allowEnterKey={false} value={(value) => handleInputChange('subtitle', value || '')} class={styles.subtitleInput} @@ -455,7 +457,7 @@ export const EditView = (props: Props) => { - }> + }> { return cyrillicRegex.test(s) } +/** + * Translates the author's name based on the provided language. For English (`lng === 'en'`), it transliterates + * and capitalizes Cyrillic names, handling special cases for characters like 'ё' and 'ь'. + * @param author - The author object containing the name to translate. + * @param lng - The target language for translation ('en' or 'ru'). + * @returns The translated author name, or the original if no translation is needed. + */ export const translateAuthor = (author: Author, lng: string) => lng === 'en' && isCyrillic(author?.name || '') ? capitalize( @@ -15,6 +27,15 @@ export const translateAuthor = (author: Author, lng: string) => ) : author.name +/** + * Reduces a list of authors into groups based on the first readable letter of their last name. + * The grouping depends on the language ('ru' for Russian and 'en' for English). + * Non-Cyrillic or non-Latin characters are grouped under `@`. + * @param acc - The accumulator object for grouping authors by the first readable letter. + * @param author - The author object containing the name. + * @param lng - The language code ('en' or 'ru') used for transliteration and sorting. + * @returns The accumulator object with authors grouped by the first readable letter of their last name. + */ export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Author, lng: string) => { let letter = '' diff --git a/src/lib/editorOptions.ts b/src/lib/editorExtensions.ts similarity index 83% rename from src/lib/editorOptions.ts rename to src/lib/editorExtensions.ts index f7686d7f..b6603987 100644 --- a/src/lib/editorOptions.ts +++ b/src/lib/editorExtensions.ts @@ -1,9 +1,15 @@ import { EditorOptions } from '@tiptap/core' +import Dropcursor from '@tiptap/extension-dropcursor' +import Focus from '@tiptap/extension-focus' +import Gapcursor from '@tiptap/extension-gapcursor' +import HardBreak from '@tiptap/extension-hard-break' import Highlight from '@tiptap/extension-highlight' +import HorizontalRule from '@tiptap/extension-horizontal-rule' import Image from '@tiptap/extension-image' import Link from '@tiptap/extension-link' import Underline from '@tiptap/extension-underline' import StarterKit from '@tiptap/starter-kit' +import ArticleNode from '~/components/Editor/extensions/Article' import { CustomBlockquote } from '~/components/Editor/extensions/CustomBlockquote' import { Figcaption } from '~/components/Editor/extensions/Figcaption' import { Figure } from '~/components/Editor/extensions/Figure' @@ -53,9 +59,17 @@ export const custom: EditorOptions['extensions'] = [ ] export const extended: EditorOptions['extensions'] = [ + HorizontalRule.configure({ HTMLAttributes: { class: 'horizontalRule' } }), + Highlight.configure({ multicolor: true, HTMLAttributes: { class: 'highlight' } }), + Dropcursor, + CustomBlockquote, + Span, + ToggleTextWrap, Footnote, - CustomBlockquote - // TODO: Добавьте другие кастомные расширения здесь + Focus, + Gapcursor, + HardBreak, + ArticleNode ] /* diff --git a/vite.config.ts b/vite.config.ts index 5f3ee698..19278941 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,23 +1,23 @@ // biome-ignore lint/correctness/noNodejsModules: used during build import path from 'node:path' +// import { visualizer } from 'rollup-plugin-visualizer' +import dotenv from 'dotenv' import { CSSOptions } from 'vite' import mkcert from 'vite-plugin-mkcert' import { PolyfillOptions, nodePolyfills } from 'vite-plugin-node-polyfills' import sassDts from 'vite-plugin-sass-dts' -// import { visualizer } from 'rollup-plugin-visualizer' -const isDev = process.env.NODE_ENV !== 'production' -console.log(`[vite.config] development mode: ${isDev}`) +// Load environment variables from .env file +dotenv.config() + +export const isDev = process.env.NODE_ENV !== 'production' +console.log(`[vite.config] ${process.env.NODE_ENV} mode`) const polyfillOptions = { include: ['path', 'stream', 'util'], exclude: ['http'], - globals: { - Buffer: true - }, - overrides: { - fs: 'memfs' - }, + globals: { Buffer: true }, + overrides: { fs: 'memfs' }, protocolImports: true } as PolyfillOptions @@ -45,12 +45,19 @@ export default { build: { target: 'esnext', sourcemap: true, + minify: 'terser', // explicit terser usage + terserOptions: { + compress: { + drop_console: true // removes console logs in production + } + }, rollupOptions: { // plugins: [visualizer()] output: { manualChunks: { icons: ['./src/components/_shared/Icon/Icon.tsx'], session: ['./src/context/session.tsx'], + localize: ['./src/context/localize.tsx'], editor: ['./src/context/editor.tsx'], connect: ['./src/context/connect.tsx'] }