linted-update

This commit is contained in:
Untone 2024-09-15 21:43:35 +03:00
parent 7d895aa343
commit ba55780246
37 changed files with 171 additions and 160 deletions

View File

@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json", "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
"files": { "files": {
"include": ["*.tsx", "*.ts", "*.js", "*.json"], "include": ["*.tsx", "*.ts", "*.js", "*.json"],
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"] "ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
@ -42,7 +42,9 @@
"noExcessiveCognitiveComplexity": "off" "noExcessiveCognitiveComplexity": "off"
}, },
"correctness": { "correctness": {
"useHookAtTopLevel": "off" "useHookAtTopLevel": "off",
"useImportExtensions": "off",
"noUndeclaredDependencies": "off"
}, },
"a11y": { "a11y": {
"useHeadingContent": "off", "useHeadingContent": "off",
@ -54,7 +56,8 @@
"useAltText": "off", "useAltText": "off",
"useButtonType": "off", "useButtonType": "off",
"noRedundantAlt": "off", "noRedundantAlt": "off",
"noSvgWithoutTitle": "off" "noSvgWithoutTitle": "off",
"noLabelWithoutControl": "off"
}, },
"nursery": { "nursery": {
"useImportRestrictions": "off" "useImportRestrictions": "off"
@ -63,15 +66,18 @@
"noBarrelFile": "off" "noBarrelFile": "off"
}, },
"style": { "style": {
"noNonNullAssertion": "off",
"noNamespaceImport": "warn", "noNamespaceImport": "warn",
"useBlockStatements": "off", "useBlockStatements": "off",
"noImplicitBoolean": "off", "noImplicitBoolean": "off",
"useNamingConvention": "off", "useNamingConvention": "off",
"useImportType": "off", "useImportType": "off",
"noDefaultExport": "off", "noDefaultExport": "off",
"useFilenamingConvention": "off" "useFilenamingConvention": "off",
"useExplicitLengthCheck": "off"
}, },
"suspicious": { "suspicious": {
"noConsole": "off",
"noConsoleLog": "off", "noConsoleLog": "off",
"noAssignInExpressions": "off" "noAssignInExpressions": "off"
} }

View File

@ -22,13 +22,13 @@
}, },
"devDependencies": { "devDependencies": {
"@authorizerdev/authorizer-js": "^2.0.3", "@authorizerdev/authorizer-js": "^2.0.3",
"@biomejs/biome": "^1.8.3", "@biomejs/biome": "^1.9.1",
"@graphql-codegen/cli": "^5.0.2", "@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/typescript": "^4.0.9", "@graphql-codegen/typescript": "^4.0.9",
"@graphql-codegen/typescript-operations": "^4.2.3", "@graphql-codegen/typescript-operations": "^4.2.3",
"@graphql-codegen/typescript-urql": "^4.0.0", "@graphql-codegen/typescript-urql": "^4.0.0",
"@hocuspocus/provider": "^2.13.5", "@hocuspocus/provider": "^2.13.5",
"@playwright/test": "^1.46.1", "@playwright/test": "^1.47.1",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@solid-primitives/media": "^2.2.9", "@solid-primitives/media": "^2.2.9",
"@solid-primitives/memo": "^1.3.9", "@solid-primitives/memo": "^1.3.9",
@ -38,22 +38,22 @@
"@solid-primitives/storage": "^3.8.0", "@solid-primitives/storage": "^3.8.0",
"@solid-primitives/upload": "^0.0.117", "@solid-primitives/upload": "^0.0.117",
"@solidjs/meta": "^0.29.4", "@solidjs/meta": "^0.29.4",
"@solidjs/router": "^0.14.3", "@solidjs/router": "^0.14.5",
"@solidjs/start": "^1.0.6", "@solidjs/start": "^1.0.6",
"@storybook/addon-a11y": "^8.2.9", "@storybook/addon-a11y": "^8.3.0",
"@storybook/addon-actions": "^8.2.9", "@storybook/addon-actions": "^8.3.0",
"@storybook/addon-controls": "^8.2.9", "@storybook/addon-controls": "^8.3.0",
"@storybook/addon-essentials": "^8.2.9", "@storybook/addon-essentials": "^8.3.0",
"@storybook/addon-interactions": "^8.2.9", "@storybook/addon-interactions": "^8.3.0",
"@storybook/addon-links": "^8.2.9", "@storybook/addon-links": "^8.3.0",
"@storybook/addon-styling": "1.3.7", "@storybook/addon-styling": "1.3.7",
"@storybook/addon-themes": "^8.2.9", "@storybook/addon-themes": "^8.3.0",
"@storybook/addon-viewport": "^8.2.9", "@storybook/addon-viewport": "^8.3.0",
"@storybook/blocks": "^8.2.9", "@storybook/blocks": "^8.3.0",
"@storybook/builder-vite": "8.2.9", "@storybook/builder-vite": "8.2.9",
"@storybook/docs-tools": "8.2.9", "@storybook/docs-tools": "8.2.9",
"@storybook/html": "^8.2.9", "@storybook/html": "^8.3.0",
"@storybook/react": "^8.2.9", "@storybook/react": "^8.3.0",
"@storybook/test-runner": "^0.19.1", "@storybook/test-runner": "^0.19.1",
"@storybook/testing-library": "^0.2.2", "@storybook/testing-library": "^0.2.2",
"@tiptap/core": "^2.6.6", "@tiptap/core": "^2.6.6",
@ -87,7 +87,7 @@
"@tiptap/extension-youtube": "^2.6.6", "@tiptap/extension-youtube": "^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.5",
"@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",
@ -96,10 +96,10 @@
"cookie": "^0.6.0", "cookie": "^0.6.0",
"cookie-signature": "^1.2.1", "cookie-signature": "^1.2.1",
"cropperjs": "^1.6.2", "cropperjs": "^1.6.2",
"extended-eventsource": "^1.4.9", "extended-eventsource": "^1.6.4",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"graphql": "^16.9.0", "graphql": "^16.9.0",
"i18next": "^23.14.0", "i18next": "^23.15.1",
"i18next-http-backend": "^2.6.1", "i18next-http-backend": "^2.6.1",
"i18next-icu": "^2.3.0", "i18next-icu": "^2.3.0",
"intl-messageformat": "^10.5.14", "intl-messageformat": "^10.5.14",
@ -107,31 +107,31 @@
"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.3.0",
"storybook-solidjs": "^1.0.0-beta.2", "storybook-solidjs": "^1.0.0-beta.2",
"storybook-solidjs-vite": "^1.0.0-beta.2", "storybook-solidjs-vite": "^1.0.0-beta.2",
"stylelint": "^16.9.0", "stylelint": "^16.9.0",
"stylelint-config-recommended": "^14.0.1", "stylelint-config-recommended": "^14.0.1",
"stylelint-config-standard-scss": "^13.1.0", "stylelint-config-standard-scss": "^13.1.0",
"stylelint-order": "^6.0.4", "stylelint-order": "^6.0.4",
"stylelint-scss": "^6.5.1", "stylelint-scss": "^6.6.0",
"swiper": "^11.1.12", "swiper": "^11.1.14",
"throttle-debounce": "^5.0.2", "throttle-debounce": "^5.0.2",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"typescript": "^5.5.4", "typescript": "^5.6.2",
"typograf": "^7.4.1", "typograf": "^7.4.1",
"uniqolor": "^1.1.1", "uniqolor": "^1.1.1",
"vinxi": "^0.4.2", "vinxi": "^0.4.3",
"vite-plugin-mkcert": "^1.17.6", "vite-plugin-mkcert": "^1.17.6",
"vite-plugin-node-polyfills": "^0.22.0", "vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-sass-dts": "^1.3.25", "vite-plugin-sass-dts": "^1.3.29",
"y-prosemirror": "1.2.12", "y-prosemirror": "1.2.12",
"yjs": "13.6.18" "yjs": "13.6.18"
}, },

View File

@ -74,7 +74,7 @@ export const PlayerHeader = (props: Props) => {
onChange={({ target }) => props.onVolumeChange(Number(target.value))} onChange={({ target }) => props.onVolumeChange(Number(target.value))}
/> />
</Show> </Show>
<button onClick={toggleVolumeBar} class={styles.volumeButton} role="button" aria-label="Volume"> <button onClick={toggleVolumeBar} class={styles.volumeButton} aria-label="Volume">
<Icon name="volume" /> <Icon name="volume" />
</button> </button>
</div> </div>

View File

@ -84,7 +84,6 @@ export const CommentRatingControl = (props: Props) => {
return ( return (
<div class={styles.commentRating}> <div class={styles.commentRating}>
<button <button
role="button"
disabled={!(canVote() && uid())} disabled={!(canVote() && uid())}
onClick={() => handleRatingChange(true)} onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, { class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
@ -110,7 +109,6 @@ export const CommentRatingControl = (props: Props) => {
/> />
</Popup> </Popup>
<button <button
role="button"
disabled={!(canVote() && uid())} disabled={!(canVote() && uid())}
onClick={() => handleRatingChange(false)} onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, { class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {

View File

@ -112,7 +112,7 @@ export const FullArticle = (props: Props) => {
const mainTopicSlug = (props.article.topics?.length || 0) > 0 ? props.article.main_topic : null const mainTopicSlug = (props.article.topics?.length || 0) > 0 ? props.article.main_topic : null
const mt = props.article.topics?.find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug) const mt = props.article.topics?.find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
if (mt) { if (mt) {
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title mt.title = lang() === 'en' ? capitalize(mt.slug.replaceAll('-', ' ')) : mt.title
return mt return mt
} }
return props.article.topics?.[0] return props.article.topics?.[0]
@ -326,7 +326,7 @@ export const FullArticle = (props: Props) => {
const shareUrl = createMemo(() => getShareUrl({ pathname: `/${props.article.slug || ''}` })) const shareUrl = createMemo(() => getShareUrl({ pathname: `/${props.article.slug || ''}` }))
const getAuthorName = (a: Author) => const getAuthorName = (a: Author) =>
lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replace(/-/, ' ')) : a.name lang() === 'en' && isCyrillic(a.name || '') ? capitalize(a.slug.replaceAll('-', ' ')) : a.name
return ( return (
<> <>
<For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For> <For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For>

View File

@ -22,7 +22,7 @@ export const Userpic = (props: Props) => {
const letters = () => { const letters = () => {
if (!props.name) return if (!props.name) return
const names = props.name ? props.name.split(' ') : [] const names = props.name ? props.name.split(' ') : []
return `${names[0][0 ?? names[0][0]]}.${names.length > 1 ? `${names[1][0]}.` : ''}` return `${names[0][0] ? names[0][0] : ''}.${names.length > 1 ? `${names[1][0]}.` : ''}`
} }
const avatarSize = createMemo(() => { const avatarSize = createMemo(() => {

View File

@ -22,7 +22,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => { onClick={() => {
props.editor.chain().focus().setBlockQuoteFloat('left').run() props.editor?.chain().focus().setBlockQuoteFloat('left').run()
}} }}
> >
<Icon name="editor-image-align-left" /> <Icon name="editor-image-align-left" />
@ -35,7 +35,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setBlockQuoteFloat(null).run()} onClick={() => props.editor?.chain().focus().setBlockQuoteFloat(null).run()}
> >
<Icon name="editor-image-align-center" /> <Icon name="editor-image-align-center" />
</button> </button>
@ -47,7 +47,7 @@ export const BlockquoteBubbleMenu = (props: Props) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setBlockQuoteFloat('right').run()} onClick={() => props.editor?.chain().focus().setBlockQuoteFloat('right').run()}
> >
<Icon name="editor-image-align-right" /> <Icon name="editor-image-align-right" />
</button> </button>

View File

@ -33,7 +33,7 @@ export const FigureBubbleMenu = (props: Props) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setFigureFloat('left').run()} onClick={() => props.editor?.chain().focus().setFigureFloat('left').run()}
> >
<Icon name="editor-image-align-left" /> <Icon name="editor-image-align-left" />
</button> </button>
@ -45,7 +45,7 @@ export const FigureBubbleMenu = (props: Props) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setFigureFloat(null).run()} onClick={() => props.editor?.chain().focus().setFigureFloat(null).run()}
> >
<Icon name="editor-image-align-center" /> <Icon name="editor-image-align-center" />
</button> </button>
@ -57,7 +57,7 @@ export const FigureBubbleMenu = (props: Props) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setFigureFloat('right').run()} onClick={() => props.editor?.chain().focus().setFigureFloat('right').run()}
> >
<Icon name="editor-image-align-right" /> <Icon name="editor-image-align-right" />
</button> </button>
@ -67,7 +67,7 @@ export const FigureBubbleMenu = (props: Props) => {
<button <button
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setFigcaptionFocus(true).run()} onClick={() => props.editor?.chain().focus().setFigcaptionFocus(true).run()}
> >
<span style={{ color: 'white' }}>{t('Add signature')}</span> <span style={{ color: 'white' }}>{t('Add signature')}</span>
</button> </button>

View File

@ -19,7 +19,7 @@ export const IncutBubbleMenu = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const [substratBubbleOpen, setSubstratBubbleOpen] = createSignal(false) const [substratBubbleOpen, setSubstratBubbleOpen] = createSignal(false)
const handleChangeBg = (bg: string | null) => { const handleChangeBg = (bg: string | null) => {
props.editor.chain().focus().setArticleBg(bg).run() props.editor?.chain().focus().setArticleBg(bg).run()
setSubstratBubbleOpen(false) setSubstratBubbleOpen(false)
} }
return ( return (
@ -27,14 +27,14 @@ export const IncutBubbleMenu = (props: Props) => {
<button <button
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat('half-left').run()} onClick={() => props.editor?.chain().focus().setArticleFloat('half-left').run()}
> >
<Icon name="editor-image-half-align-left" /> <Icon name="editor-image-half-align-left" />
</button> </button>
<button <button
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat(null).run()} onClick={() => props.editor?.chain().focus().setArticleFloat(null).run()}
> >
<Icon name="editor-image-align-center" /> <Icon name="editor-image-align-center" />
</button> </button>
@ -42,7 +42,7 @@ export const IncutBubbleMenu = (props: Props) => {
<button <button
type="button" type="button"
class={styles.bubbleMenuButton} class={styles.bubbleMenuButton}
onClick={() => props.editor.chain().focus().setArticleFloat('half-right').run()} onClick={() => props.editor?.chain().focus().setArticleFloat('half-right').run()}
> >
<Icon name="editor-image-half-align-right" /> <Icon name="editor-image-half-align-right" />
</button> </button>

View File

@ -77,16 +77,14 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
} }
createEffect(() => { createEffect(() => {
switch (selectedMenuItem()) { if (selectedMenuItem() === 'image') {
case 'image': { showModal('uploadImage')
showModal('uploadImage') return
return }
} if (selectedMenuItem() === 'horizontal-rule') {
case 'horizontal-rule': { props.editor?.chain().focus().setHorizontalRule().run()
props.editor?.chain().focus().setHorizontalRule().run() setSelectedMenuItem()
setSelectedMenuItem() return
return
}
} }
}) })

View File

@ -91,9 +91,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
const handleAddFootnote = (footnote: string) => { const handleAddFootnote = (footnote: string) => {
if (footNote()) { if (footNote()) {
props.editor.chain().focus().updateFootnote({ value: footnote }).run() props.editor?.chain().focus().updateFootnote({ value: footnote }).run()
} else { } else {
props.editor.chain().focus().setFootnote({ value: footnote }).run() props.editor?.chain().focus().setFootnote({ value: footnote }).run()
} }
setFootNote() setFootNote()
setLinkEditorOpen(false) setLinkEditorOpen(false)
@ -108,16 +108,16 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
const handleSetPunchline = () => { const handleSetPunchline = () => {
if (isPunchLine()) { if (isPunchLine()) {
props.editor.chain().focus().toggleBlockquote('punchline').run() props.editor?.chain().focus().toggleBlockquote('punchline').run()
} }
props.editor.chain().focus().toggleBlockquote('quote').run() props.editor?.chain().focus().toggleBlockquote('quote').run()
toggleTextSizePopup() toggleTextSizePopup()
} }
const handleSetQuote = () => { const handleSetQuote = () => {
if (isQuote()) { if (isQuote()) {
props.editor.chain().focus().toggleBlockquote('quote').run() props.editor?.chain().focus().toggleBlockquote('quote').run()
} }
props.editor.chain().focus().toggleBlockquote('punchline').run() props.editor?.chain().focus().toggleBlockquote('punchline').run()
toggleTextSizePopup() toggleTextSizePopup()
} }
@ -130,13 +130,13 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
}) })
const handleOpenLinkForm = () => { const handleOpenLinkForm = () => {
props.editor.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run() props.editor?.chain().focus().addTextWrap({ class: 'highlight-fake-selection' }).run()
setLinkEditorOpen(true) setLinkEditorOpen(true)
} }
const handleCloseLinkForm = () => { const handleCloseLinkForm = () => {
setLinkEditorOpen(false) setLinkEditorOpen(false)
props.editor.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run() props.editor?.chain().focus().removeTextWrap({ class: 'highlight-fake-selection' }).run()
} }
return ( return (
@ -188,7 +188,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
[styles.bubbleMenuButtonActive]: isH1() [styles.bubbleMenuButtonActive]: isH1()
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 2 }).run() props.editor?.chain().focus().toggleHeading({ level: 2 }).run()
toggleTextSizePopup() toggleTextSizePopup()
}} }}
> >
@ -205,7 +205,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
[styles.bubbleMenuButtonActive]: isH2() [styles.bubbleMenuButtonActive]: isH2()
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 3 }).run() props.editor?.chain().focus().toggleHeading({ level: 3 }).run()
toggleTextSizePopup() toggleTextSizePopup()
}} }}
> >
@ -222,7 +222,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
[styles.bubbleMenuButtonActive]: isH3() [styles.bubbleMenuButtonActive]: isH3()
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 4 }).run() props.editor?.chain().focus().toggleHeading({ level: 4 }).run()
toggleTextSizePopup() toggleTextSizePopup()
}} }}
> >
@ -273,7 +273,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
[styles.bubbleMenuButtonActive]: isIncut() [styles.bubbleMenuButtonActive]: isIncut()
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleArticle().run() props.editor?.chain().focus().toggleArticle().run()
toggleTextSizePopup() toggleTextSizePopup()
}} }}
> >
@ -296,7 +296,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBold() [styles.bubbleMenuButtonActive]: isBold()
})} })}
onClick={() => props.editor.chain().focus().toggleBold().run()} onClick={() => props.editor?.chain().focus().toggleBold().run()}
> >
<Icon name="editor-bold" /> <Icon name="editor-bold" />
</button> </button>
@ -310,7 +310,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isItalic() [styles.bubbleMenuButtonActive]: isItalic()
})} })}
onClick={() => props.editor.chain().focus().toggleItalic().run()} onClick={() => props.editor?.chain().focus().toggleItalic().run()}
> >
<Icon name="editor-italic" /> <Icon name="editor-italic" />
</button> </button>
@ -326,7 +326,9 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isHighlight() [styles.bubbleMenuButtonActive]: isHighlight()
})} })}
onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()} onClick={() =>
props.editor?.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()
}
> >
<div class={styles.toggleHighlight} /> <div class={styles.toggleHighlight} />
</button> </button>
@ -389,7 +391,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
[styles.bubbleMenuButtonActive]: isBulletList() [styles.bubbleMenuButtonActive]: isBulletList()
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleBulletList().run() props.editor?.chain().focus().toggleBulletList().run()
toggleListPopup() toggleListPopup()
}} }}
> >
@ -406,7 +408,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
[styles.bubbleMenuButtonActive]: isOrderedList() [styles.bubbleMenuButtonActive]: isOrderedList()
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleOrderedList().run() props.editor?.chain().focus().toggleOrderedList().run()
toggleListPopup() toggleListPopup()
}} }}
> >

View File

@ -38,7 +38,7 @@ export const Figure = Node.create({
} }
const img = node.querySelector('img') const img = node.querySelector('img')
const iframe = node.querySelector('iframe') const iframe = node.querySelector('iframe')
let dataType = null let dataType: string | undefined
if (img) { if (img) {
dataType = 'image' dataType = 'image'
} else if (iframe) { } else if (iframe) {

View File

@ -7,6 +7,7 @@ import { Popover } from '~/components/_shared/Popover'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen' import type { Author, Maybe, Shout, Topic } from '~/graphql/schema/core.gen'
import { sentenceSeparator } from '~/intl/chars'
import { capitalize } from '~/utils/capitalize' import { capitalize } from '~/utils/capitalize'
import { descFromBody } from '~/utils/meta' import { descFromBody } from '~/utils/meta'
import { CoverImage } from '../../Article/CoverImage' import { CoverImage } from '../../Article/CoverImage'
@ -69,7 +70,7 @@ const getTitleAndSubtitle = (
let titleParts = article.title?.split('. ') || [] let titleParts = article.title?.split('. ') || []
if (titleParts?.length === 1) { if (titleParts?.length === 1) {
titleParts = article.title?.split(/{!|\?|:|;}\s/) || [] titleParts = article.title?.split(sentenceSeparator) || []
} }
if (titleParts && titleParts.length > 1) { if (titleParts && titleParts.length > 1) {
@ -88,7 +89,7 @@ const getMainTopicTitle = (article: Shout, lng: string) => {
const mainTopicSlug = article.main_topic || '' const mainTopicSlug = article.main_topic || ''
const mainTopic = (article.topics || []).find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug) const mainTopic = (article.topics || []).find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
const mainTopicTitle = const mainTopicTitle =
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || '' mainTopicSlug && lng === 'en' ? mainTopicSlug.replaceAll('-', ' ') : mainTopic?.title || ''
return [mainTopicTitle, mainTopicSlug] return [mainTopicTitle, mainTopicSlug]
} }

View File

@ -33,7 +33,6 @@ export const FeedArticlePopup = (props: Props) => {
<li> <li>
<button <button
class={styles.action} class={styles.action}
role="button"
onClick={() => { onClick={() => {
props.onShareClick() props.onShareClick()
setHidePopup(true) setHidePopup(true)
@ -47,7 +46,6 @@ export const FeedArticlePopup = (props: Props) => {
<li> <li>
<button <button
class={styles.action} class={styles.action}
role="button"
onClick={() => { onClick={() => {
alert('Help to edit') alert('Help to edit')
setHidePopup(true) setHidePopup(true)
@ -61,7 +59,6 @@ export const FeedArticlePopup = (props: Props) => {
<li> <li>
<button <button
class={styles.action} class={styles.action}
role="button"
onClick={() => { onClick={() => {
props.onInviteClick() props.onInviteClick()
setHidePopup(false) setHidePopup(false)
@ -73,7 +70,7 @@ export const FeedArticlePopup = (props: Props) => {
</li> </li>
<Show when={!props.canEdit}> <Show when={!props.canEdit}>
<li> <li>
<button class={clsx(styles.action, styles.soon)} role="button"> <button class={clsx(styles.action, styles.soon)}>
<Icon name="bell-white" class={styles.icon} /> <Icon name="bell-white" class={styles.icon} />
<div class={styles.title}>{t('Subscribe to comments')}</div> <div class={styles.title}>{t('Subscribe to comments')}</div>
<SoonChip /> <SoonChip />
@ -81,7 +78,7 @@ export const FeedArticlePopup = (props: Props) => {
</li> </li>
</Show> </Show>
<li> <li>
<button class={clsx(styles.action, styles.soon)} role="button"> <button class={clsx(styles.action, styles.soon)}>
<Icon name="bookmark" class={styles.icon} /> <Icon name="bookmark" class={styles.icon} />
<div class={styles.title}>{t('Add to bookmarks')}</div> <div class={styles.title}>{t('Add to bookmarks')}</div>
<SoonChip /> <SoonChip />
@ -91,7 +88,7 @@ export const FeedArticlePopup = (props: Props) => {
{/* <li>*/} {/* <li>*/}
{/* <button*/} {/* <button*/}
{/* class={styles.action}*/} {/* class={styles.action}*/}
{/* role="button"*/} {/* */}
{/* onClick={() => {*/} {/* onClick={() => {*/}
{/* alert('Complain')*/} {/* alert('Complain')*/}
{/* }}*/} {/* }}*/}
@ -103,7 +100,7 @@ export const FeedArticlePopup = (props: Props) => {
{/*<li>*/} {/*<li>*/}
{/* <button*/} {/* <button*/}
{/* class={styles.action}*/} {/* class={styles.action}*/}
{/* role="button"*/} {/* */}
{/* onClick={() => {*/} {/* onClick={() => {*/}
{/* alert('Get notifications')*/} {/* alert('Get notifications')*/}
{/* }}*/} {/* }}*/}

View File

@ -1,6 +1,4 @@
@mixin search-filter-control { @mixin search-filter-control {
@include font-size(1.4rem);
height: 4rem; height: 4rem;
padding: 0 2rem; padding: 0 2rem;
background: rgb(64 64 64 / 50%); background: rgb(64 64 64 / 50%);
@ -9,6 +7,8 @@
font-weight: 500; font-weight: 500;
white-space: nowrap; white-space: nowrap;
@include font-size(1.4rem);
&:hover { &:hover {
background: #404040; background: #404040;
} }
@ -23,8 +23,6 @@
} }
.searchInput { .searchInput {
@include font-size(4.8rem);
width: 100%; width: 100%;
padding: 0 0 0.5rem; padding: 0 0 0.5rem;
background: none; background: none;
@ -34,6 +32,8 @@
font-weight: bold; font-weight: bold;
outline: none; outline: none;
@include font-size(4.8rem);
&::placeholder { &::placeholder {
color: rgb(255 255 255 / 32%); color: rgb(255 255 255 / 32%);
} }
@ -60,10 +60,10 @@
} }
.searchDescription { .searchDescription {
@include font-size(1.6rem);
margin-bottom: 44px; margin-bottom: 44px;
color: rgb(255 255 255 / 64%); color: rgb(255 255 255 / 64%);
@include font-size(1.6rem);
} }
.topicsList { .topicsList {

View File

@ -35,7 +35,7 @@ export const FullTopic = (props: Props) => {
/* FIXME: use title translation*/ /* FIXME: use title translation*/
setTitle((_) => tpc?.title || '') setTitle((_) => tpc?.title || '')
return `#${capitalize( return `#${capitalize(
lang() === 'en' ? tpc.slug.replace(/-/, ' ') : tpc.title || tpc.slug.replace(/-/, ' '), lang() === 'en' ? tpc.slug.replaceAll('-', ' ') : tpc.title || tpc.slug.replaceAll('-', ' '),
true true
)}` )}`
}, },

View File

@ -72,7 +72,7 @@
line-height: 1.4; line-height: 1.4;
margin: 0.8rem 0; margin: 0.8rem 0;
-webkit-line-clamp: 2; line-clamp: 2;
} }
} }

View File

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

View File

@ -6,6 +6,7 @@ import { SearchField } from '~/components/_shared/SearchField'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useTopics } from '~/context/topics' import { useTopics } from '~/context/topics'
import type { Topic } from '~/graphql/schema/core.gen' import type { Topic } from '~/graphql/schema/core.gen'
import { enChars, ruChars } from '~/intl/chars'
import { dummyFilter } from '~/intl/dummyFilter' import { dummyFilter } from '~/intl/dummyFilter'
import { scrollHandler } from '~/utils/scroll' import { scrollHandler } from '~/utils/scroll'
import { TopicBadge } from '../../Topic/TopicBadge' import { TopicBadge } from '../../Topic/TopicBadge'
@ -35,8 +36,8 @@ export const AllTopics = (props: Props) => {
return topics().reduce( return topics().reduce(
(acc, topic) => { (acc, topic) => {
let letter = lang() === 'en' ? topic.slug[0].toUpperCase() : (topic?.title?.[0] || '').toUpperCase() let letter = lang() === 'en' ? topic.slug[0].toUpperCase() : (topic?.title?.[0] || '').toUpperCase()
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '#' if (enChars.test(letter) && lang() === 'ru') letter = '#'
if (/[^A-z]/.test(letter) && lang() === 'en') letter = '#' if (ruChars.test(letter) && lang() === 'en') letter = '#'
if (!acc[letter]) acc[letter] = [] if (!acc[letter]) acc[letter] = []
acc[letter].push(topic) acc[letter].push(topic)
return acc return acc

View File

@ -250,7 +250,7 @@ export const ProfileSecurityView = (_props: any) => {
class={clsx( class={clsx(
styles.socialButton, styles.socialButton,
styles.socialButtonApple, styles.socialButtonApple,
'button' + ' button--light' 'button button--light'
)} )}
type="button" type="button"
> >

View File

@ -339,15 +339,15 @@ export const ProfileSettings = () => {
maxLength={120} maxLength={120}
/> />
<h4>{t('About the author')}</h4> <h4>{t('About')}</h4>
<SimplifiedEditor <SimplifiedEditor
resetToInitial={clearAbout()} resetToInitial={clearAbout()}
noLimits={true} noLimits={true}
variant="bordered" variant="bordered"
onlyBubbleControls={true} onlyBubbleControls={true}
smallHeight={true} smallHeight={true}
placeholder={t('About the author')} placeholder={t('About')}
label={t('About the author')} label={t('About')}
initialContent={form.about || ''} initialContent={form.about || ''}
autoFocus={false} autoFocus={false}
onChange={(value) => updateFormField('about', value)} onChange={(value) => updateFormField('about', value)}

View File

@ -37,14 +37,12 @@ export const DropArea = (props: Props) => {
const runUpload = async (files: UploadFile[]) => { const runUpload = async (files: UploadFile[]) => {
try { try {
setLoading(true) setLoading(true)
const tkn = session()?.access_token as string
const results = [] const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload
for (const file of files) { tkn &&
const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload Promise.all(files.map((file) => handler(file, tkn)))
const result = await handler(file, session()?.access_token as string) .then(props.onUpload)
results.push(result) .catch(console.error)
}
props.onUpload(results)
setLoading(false) setLoading(false)
} catch (error) { } catch (error) {
setLoading(false) setLoading(false)

View File

@ -61,7 +61,7 @@ export const Popover = (props: Props) => {
<> <>
{props.children(setAnchor)} {props.children(setAnchor)}
<Show when={show() && !props.disabled}> <Show when={show() && !props.disabled}>
<div ref={setPopper} class={styles.tooltip} role="tooltip"> <div ref={setPopper} class={styles.tooltip}>
{props.content} {props.content}
<div class={styles.arrow} data-popper-arrow={true} /> <div class={styles.arrow} data-popper-arrow={true} />
</div> </div>

View File

@ -55,7 +55,6 @@ export const ShareLinks = (props: Props) => {
<ul class="nodash"> <ul class="nodash">
<li> <li>
<button <button
role="button"
class={clsx(styles.shareControl, popupStyles.action)} class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(FACEBOOK)} onClick={() => handleShare(FACEBOOK)}
> >
@ -65,7 +64,6 @@ export const ShareLinks = (props: Props) => {
</li> </li>
<li> <li>
<button <button
role="button"
class={clsx(styles.shareControl, popupStyles.action)} class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(TWITTER)} onClick={() => handleShare(TWITTER)}
> >
@ -75,7 +73,6 @@ export const ShareLinks = (props: Props) => {
</li> </li>
<li> <li>
<button <button
role="button"
class={clsx(styles.shareControl, popupStyles.action)} class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(TELEGRAM)} onClick={() => handleShare(TELEGRAM)}
> >
@ -84,11 +81,7 @@ export const ShareLinks = (props: Props) => {
</button> </button>
</li> </li>
<li> <li>
<button <button class={clsx(styles.shareControl, popupStyles.action)} onClick={() => handleShare(VK)}>
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={() => handleShare(VK)}
>
<Icon name="vk-white" class={clsx(styles.icon, popupStyles.icon)} /> <Icon name="vk-white" class={clsx(styles.icon, popupStyles.icon)} />
VK VK
</button> </button>
@ -97,11 +90,7 @@ export const ShareLinks = (props: Props) => {
<Show <Show
when={props.variant === 'inModal'} when={props.variant === 'inModal'}
fallback={ fallback={
<button <button class={clsx(styles.shareControl, popupStyles.action)} onClick={copyLink}>
role="button"
class={clsx(styles.shareControl, popupStyles.action)}
onClick={copyLink}
>
<Icon name="link-white" class={clsx(styles.icon, popupStyles.icon)} /> <Icon name="link-white" class={clsx(styles.icon, popupStyles.icon)} />
{t('Copy link')} {t('Copy link')}
</button> </button>

View File

@ -16,7 +16,9 @@ type Props = {
onVideoDelete?: () => void onVideoDelete?: () => void
articleView?: boolean articleView?: boolean
} }
const watchPattern = /watch=(\w+)/
const ytPattern = /(youtu.be)\/(\w+)/
const vimeoPattern = /vimeo.com\/(\d+)/
export const VideoPlayer = (props: Props) => { export const VideoPlayer = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const [videoId, setVideoId] = createSignal<string | undefined>() const [videoId, setVideoId] = createSignal<string | undefined>()
@ -27,14 +29,14 @@ export const VideoPlayer = (props: Props) => {
setIsVimeo(!isYoutube) setIsVimeo(!isYoutube)
if (isYoutube) { if (isYoutube) {
if (props.videoUrl.includes('youtube.com')) { if (props.videoUrl.includes('youtube.com')) {
const videoIdMatch = props.videoUrl.match(/watch=(\w+)/) const videoIdMatch = props.videoUrl.match(watchPattern)
setVideoId(videoIdMatch?.[1]) setVideoId(videoIdMatch?.[1])
} else { } else {
const videoIdMatch = props.videoUrl.match(/youtu.be\/(\w+)/) const videoIdMatch = props.videoUrl.match(ytPattern)
setVideoId(videoIdMatch?.[1]) setVideoId(videoIdMatch?.[1])
} }
} else { } else {
const videoIdMatch = props.videoUrl.match(/vimeo.com\/(\d+)/) const videoIdMatch = props.videoUrl.match(vimeoPattern)
setVideoId(videoIdMatch?.[1]) setVideoId(videoIdMatch?.[1])
} }
}) })

View File

@ -151,16 +151,6 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
setFollows((prevFollows: AuthorFollowsResult) => { setFollows((prevFollows: AuthorFollowsResult) => {
const updatedFollows = { ...prevFollows } const updatedFollows = { ...prevFollows }
switch (what) { switch (what) {
case 'AUTHOR': {
if (value) {
if (!updatedFollows.authors?.some((author) => author.slug === slug)) {
updatedFollows.authors = [...(updatedFollows.authors || []), { slug } as Author]
}
} else {
updatedFollows.authors = updatedFollows.authors?.filter((author) => author.slug !== slug) || []
}
break
}
case 'TOPIC': { case 'TOPIC': {
if (value) { if (value) {
if (!updatedFollows.topics?.some((topic) => topic.slug === slug)) { if (!updatedFollows.topics?.some((topic) => topic.slug === slug)) {
@ -182,6 +172,17 @@ export const FollowingProvider = (props: { children: JSX.Element }) => {
} }
break break
} }
// case 'AUTHOR': {
default: {
if (value) {
if (!updatedFollows.authors?.some((author) => author.slug === slug)) {
updatedFollows.authors = [...(updatedFollows.authors || []), { slug } as Author]
}
} else {
updatedFollows.authors = updatedFollows.authors?.filter((author) => author.slug !== slug) || []
}
break
}
} }
return updatedFollows return updatedFollows
}) })

6
src/intl/chars.ts Normal file
View File

@ -0,0 +1,6 @@
export const allChars = /[^\dA-zА-я]/
export const slugChars = /[^\da-z]/g
export const enChars = /[^A-z]/
export const ruChars = /[^ËА-яё]/
export const sentenceSeparator = /{!|\?|:|;}\s/
export const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters

View File

@ -1,16 +1,18 @@
import { Author } from '~/graphql/schema/core.gen' import { Author } from '~/graphql/schema/core.gen'
import { capitalize } from '~/utils/capitalize' import { capitalize } from '~/utils/capitalize'
import { allChars, cyrillicRegex, enChars, ruChars } from './chars'
import { translit } from './translit' import { translit } from './translit'
export const isCyrillic = (s: string): boolean => { export const isCyrillic = (s: string): boolean => {
const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
return cyrillicRegex.test(s) return cyrillicRegex.test(s)
} }
export const translateAuthor = (author: Author, lng: string) => export const translateAuthor = (author: Author, lng: string) =>
lng === 'en' && isCyrillic(author?.name || '') lng === 'en' && isCyrillic(author?.name || '')
? capitalize(translit((author?.name || '').replace(/ё/, 'e').replace(/ь/, '')).replace(/-/, ' '), true) ? capitalize(
translit((author?.name || '').replaceAll('ё', 'e').replaceAll('ь', '')).replaceAll('-', ' '),
true
)
: author.name : author.name
export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Author, lng: string) => { export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Author, lng: string) => {
@ -18,7 +20,7 @@ export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Autho
if (!letter && author && author.name) { if (!letter && author && author.name) {
const name = const name =
translateAuthor(author, lng || 'ru') translateAuthor(author, lng || 'ru')
?.replace(/[^\dA-zА-я]/, ' ') ?.replace(allChars, ' ')
.trim() || '' .trim() || ''
const nameParts = name.trim().split(' ') const nameParts = name.trim().split(' ')
const found = nameParts.filter(Boolean).pop() const found = nameParts.filter(Boolean).pop()
@ -26,8 +28,8 @@ export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Autho
letter = found[0].toUpperCase() letter = found[0].toUpperCase()
} }
} }
if (/[^ËА-яё]/.test(letter) && lng === 'ru') letter = '@' if (ruChars.test(letter) && lng === 'ru') letter = '@'
if (/[^A-z]/.test(letter) && lng === 'en') letter = '@' if (enChars.test(letter) && lng === 'en') letter = '@'
if (!acc[letter]) acc[letter] = [] if (!acc[letter]) acc[letter] = []
author.name = translateAuthor(author, lng) author.name = translateAuthor(author, lng)

View File

@ -1,4 +1,5 @@
import translitConfig from './abc-translit.json' import translitConfig from './abc-translit.json'
import { ruChars, slugChars } from './chars'
const ru2en: { [key: string]: string } = translitConfig const ru2en: { [key: string]: string } = translitConfig
@ -7,7 +8,7 @@ export const translit = (str: string) => {
return '' return ''
} }
const isCyrillic = /[ЁА-яё]/.test(str) const isCyrillic = ruChars.test(str)
if (!isCyrillic) { if (!isCyrillic) {
return str return str
@ -17,7 +18,5 @@ export const translit = (str: string) => {
} }
export const slugify = (text: string) => { export const slugify = (text: string) => {
return translit(text.toLowerCase()) return translit(text.toLowerCase()).replaceAll(' ', '-').replaceAll(slugChars, '')
.replaceAll(' ', '-')
.replaceAll(/[^\da-z]/g, '')
} }

View File

@ -1,5 +1,7 @@
const audioExts = /\.(wav|flac|mp3|aac|jpg|jpeg|png|gif)$/i
const removeMediaFileExtension = (fileName: string) => { const removeMediaFileExtension = (fileName: string) => {
return fileName.replace(/\.(wav|flac|mp3|aac|jpg|jpeg|png|gif)$/i, '') return fileName.replace(audioExts, '')
} }
export const composeMediaItems = ( export const composeMediaItems = (

View File

@ -13,6 +13,8 @@ import { loadShouts } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
import { SHOUTS_PER_PAGE } from '../(main)' import { SHOUTS_PER_PAGE } from '../(main)'
const paramPattern = /^(hot|likes)$/
export type FeedPeriod = 'week' | 'month' | 'year' export type FeedPeriod = 'week' | 'month' | 'year'
export type PeriodItem = { export type PeriodItem = {
@ -32,14 +34,15 @@ const getFromDate = (period: FeedPeriod): number => {
d = new Date(now.setDate(now.getDate() - 7)) d = new Date(now.setDate(now.getDate() - 7))
break break
} }
case 'month': {
d = new Date(now.setMonth(now.getMonth() - 1))
break
}
case 'year': { case 'year': {
d = new Date(now.setFullYear(now.getFullYear() - 1)) d = new Date(now.setFullYear(now.getFullYear() - 1))
break break
} }
// case 'month': {
default: {
d = new Date(now.setMonth(now.getMonth() - 1))
break
}
} }
return Math.floor(d.getTime() / 1000) return Math.floor(d.getTime() / 1000)
} }
@ -71,7 +74,6 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
// load more feed // load more feed
const loadMoreFeed = async (offset?: number) => { const loadMoreFeed = async (offset?: number) => {
// /feed/:order: - select order setting // /feed/:order: - select order setting
const paramPattern = /^(hot|likes)$/
const order = const order =
(props.params.order && paramPattern.test(props.params.order) (props.params.order && paramPattern.test(props.params.order)
? props.params.order === 'hot' ? props.params.order === 'hot'
@ -107,9 +109,8 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
}) })
const order = createMemo(() => { const order = createMemo(() => {
const paramOrderPattern = /^(hot|likes)$/
return ( return (
(paramOrderPattern.test(props.params.order) (paramPattern.test(props.params.order)
? props.params.order === 'hot' ? props.params.order === 'hot'
? 'last_comment' ? 'last_comment'
: props.params.order : props.params.order

View File

@ -30,6 +30,8 @@ const feeds = {
export type FeedPeriod = 'week' | 'month' | 'year' export type FeedPeriod = 'week' | 'month' | 'year'
export type FeedSearchParams = { period?: FeedPeriod } export type FeedSearchParams = { period?: FeedPeriod }
const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/
const paramPattern = /(hot|likes)/
const getFromDate = (period: FeedPeriod): number => { const getFromDate = (period: FeedPeriod): number => {
const now = new Date() const now = new Date()
let d: Date = now let d: Date = now
@ -38,14 +40,15 @@ const getFromDate = (period: FeedPeriod): number => {
d = new Date(now.setDate(now.getDate() - 7)) d = new Date(now.setDate(now.getDate() - 7))
break break
} }
case 'month': {
d = new Date(now.setMonth(now.getMonth() - 1))
break
}
case 'year': { case 'year': {
d = new Date(now.setFullYear(now.getFullYear() - 1)) d = new Date(now.setFullYear(now.getFullYear() - 1))
break break
} }
// case 'month':
default: {
d = new Date(now.setMonth(now.getMonth() - 1))
break
}
} }
return Math.floor(d.getTime() / 1000) return Math.floor(d.getTime() / 1000)
} }
@ -67,14 +70,12 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
// /feed/my/:mode: // /feed/my/:mode:
const mode = createMemo(() => { const mode = createMemo(() => {
const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/
return props.params.mode && paramModePattern.test(props.params.mode) ? props.params.mode : 'followed' return props.params.mode && paramModePattern.test(props.params.mode) ? props.params.mode : 'followed'
}) })
const order = createMemo(() => { const order = createMemo(() => {
const paramOrderPattern = /^(hot|likes)$/
return ( return (
(paramOrderPattern.test(props.params.order) (paramPattern.test(props.params.order)
? props.params.order === 'hot' ? props.params.order === 'hot'
? 'last_comment' ? 'last_comment'
: props.params.order : props.params.order

View File

@ -1,7 +1,9 @@
const emailPattern = /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i
export const validateEmail = (email: string) => { export const validateEmail = (email: string) => {
if (!email) return false if (!email) return false
return /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email) return emailPattern.test(email)
} }
export const validateUrl = (value: string) => { export const validateUrl = (value: string) => {

View File

@ -50,6 +50,7 @@ test.beforeAll(async ({ browser }) => {
page = await browser.newPage() page = await browser.newPage()
test.setTimeout(150000) test.setTimeout(150000)
await page.goto(baseURL) await page.goto(baseURL)
// biome-ignore lint/performance/useTopLevelRegex: <explanation>
await expect(page).toHaveTitle(/Дискурс/) await expect(page).toHaveTitle(/Дискурс/)
console.log('Localhost server started successfully!') console.log('Localhost server started successfully!')
}) })

View File

@ -50,6 +50,7 @@ test.beforeAll(async ({ browser }) => {
page = await browser.newPage() page = await browser.newPage()
test.setTimeout(150000) test.setTimeout(150000)
await page.goto(baseURL) await page.goto(baseURL)
// biome-ignore lint/performance/useTopLevelRegex: <explanation>
await expect(page).toHaveTitle(/Дискурс/) await expect(page).toHaveTitle(/Дискурс/)
await page.getByRole('link', { name: 'Войти' }).click() await page.getByRole('link', { name: 'Войти' }).click()
console.log('Localhost server started successfully!') console.log('Localhost server started successfully!')

View File

@ -50,6 +50,7 @@ test.beforeAll(async ({ browser }) => {
page = await browser.newPage() page = await browser.newPage()
test.setTimeout(150000) test.setTimeout(150000)
await page.goto(baseURL) await page.goto(baseURL)
// biome-ignore lint/performance/useTopLevelRegex: <explanation>
await expect(page).toHaveTitle(/Дискурс/) await expect(page).toHaveTitle(/Дискурс/)
console.log('Localhost server started successfully!') console.log('Localhost server started successfully!')
await page.close() await page.close()

View File

@ -54,6 +54,7 @@ test.beforeAll(async ({ browser }) => {
page = await context.newPage() page = await context.newPage()
test.setTimeout(150000) test.setTimeout(150000)
await page.goto(baseURL) await page.goto(baseURL)
// biome-ignore lint/performance/useTopLevelRegex: <explanation>
await expect(page).toHaveTitle(/Дискурс/) await expect(page).toHaveTitle(/Дискурс/)
await page.getByRole('link', { name: 'Войти' }).click() await page.getByRole('link', { name: 'Войти' }).click()
console.log('Localhost server started successfully!') console.log('Localhost server started successfully!')