bad update
This commit is contained in:
parent
512c65aeef
commit
344f716d1d
13
biome.json
13
biome.json
|
@ -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": {
|
||||
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
|
||||
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"]
|
||||
|
@ -42,7 +42,9 @@
|
|||
"noExcessiveCognitiveComplexity": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"useHookAtTopLevel": "off"
|
||||
"useHookAtTopLevel": "off",
|
||||
"useImportExtensions": "off",
|
||||
"noUndeclaredDependencies": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"useHeadingContent": "off",
|
||||
|
@ -54,7 +56,8 @@
|
|||
"useAltText": "off",
|
||||
"useButtonType": "off",
|
||||
"noRedundantAlt": "off",
|
||||
"noSvgWithoutTitle": "off"
|
||||
"noSvgWithoutTitle": "off",
|
||||
"noLabelWithoutControl": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"useImportRestrictions": "off"
|
||||
|
@ -70,9 +73,11 @@
|
|||
"useNamingConvention": "off",
|
||||
"useImportType": "off",
|
||||
"noDefaultExport": "off",
|
||||
"useFilenamingConvention": "off"
|
||||
"useFilenamingConvention": "off",
|
||||
"useExplicitLengthCheck": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noConsole": "off",
|
||||
"noConsoleLog": "off",
|
||||
"noAssignInExpressions": "off"
|
||||
}
|
||||
|
|
29485
package-lock.json
generated
29485
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
54
package.json
54
package.json
|
@ -22,13 +22,13 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@authorizerdev/authorizer-js": "^2.0.3",
|
||||
"@biomejs/biome": "^1.8.3",
|
||||
"@biomejs/biome": "^1.9.1",
|
||||
"@graphql-codegen/cli": "^5.0.2",
|
||||
"@graphql-codegen/typescript": "^4.0.9",
|
||||
"@graphql-codegen/typescript-operations": "^4.2.3",
|
||||
"@graphql-codegen/typescript-urql": "^4.0.0",
|
||||
"@hocuspocus/provider": "^2.13.5",
|
||||
"@playwright/test": "^1.46.1",
|
||||
"@playwright/test": "^1.47.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@solid-primitives/media": "^2.2.9",
|
||||
"@solid-primitives/memo": "^1.3.9",
|
||||
|
@ -38,22 +38,22 @@
|
|||
"@solid-primitives/storage": "^3.8.0",
|
||||
"@solid-primitives/upload": "^0.0.117",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.14.3",
|
||||
"@solidjs/router": "^0.14.5",
|
||||
"@solidjs/start": "^1.0.6",
|
||||
"@storybook/addon-a11y": "^8.2.9",
|
||||
"@storybook/addon-actions": "^8.2.9",
|
||||
"@storybook/addon-controls": "^8.2.9",
|
||||
"@storybook/addon-essentials": "^8.2.9",
|
||||
"@storybook/addon-interactions": "^8.2.9",
|
||||
"@storybook/addon-links": "^8.2.9",
|
||||
"@storybook/addon-a11y": "^8.3.0",
|
||||
"@storybook/addon-actions": "^8.3.0",
|
||||
"@storybook/addon-controls": "^8.3.0",
|
||||
"@storybook/addon-essentials": "^8.3.0",
|
||||
"@storybook/addon-interactions": "^8.3.0",
|
||||
"@storybook/addon-links": "^8.3.0",
|
||||
"@storybook/addon-styling": "1.3.7",
|
||||
"@storybook/addon-themes": "^8.2.9",
|
||||
"@storybook/addon-viewport": "^8.2.9",
|
||||
"@storybook/blocks": "^8.2.9",
|
||||
"@storybook/addon-themes": "^8.3.0",
|
||||
"@storybook/addon-viewport": "^8.3.0",
|
||||
"@storybook/blocks": "^8.3.0",
|
||||
"@storybook/builder-vite": "8.2.9",
|
||||
"@storybook/docs-tools": "8.2.9",
|
||||
"@storybook/html": "^8.2.9",
|
||||
"@storybook/react": "^8.2.9",
|
||||
"@storybook/docs-tools": "^8.3.0",
|
||||
"@storybook/html": "^8.3.0",
|
||||
"@storybook/react": "^8.3.0",
|
||||
"@storybook/test-runner": "^0.19.1",
|
||||
"@storybook/testing-library": "^0.2.2",
|
||||
"@tiptap/core": "^2.6.6",
|
||||
|
@ -88,7 +88,7 @@
|
|||
"@tiptap/starter-kit": "^2.6.6",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/cookie-signature": "^1.1.2",
|
||||
"@types/node": "^22.5.3",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@urql/core": "^5.0.6",
|
||||
"axe-playwright": "^2.0.2",
|
||||
|
@ -97,10 +97,10 @@
|
|||
"cookie": "^0.6.0",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"cropperjs": "^1.6.2",
|
||||
"extended-eventsource": "^1.4.9",
|
||||
"extended-eventsource": "^1.6.4",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next": "^23.15.1",
|
||||
"i18next-http-backend": "^2.6.1",
|
||||
"i18next-icu": "^2.3.0",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
|
@ -115,30 +115,30 @@
|
|||
"solid-popper": "^0.3.0",
|
||||
"solid-tiptap": "^0.7.0",
|
||||
"solid-transition-group": "^0.2.3",
|
||||
"storybook": "^8.2.9",
|
||||
"storybook": "^8.3.0",
|
||||
"storybook-solidjs": "^1.0.0-beta.2",
|
||||
"storybook-solidjs-vite": "^1.0.0-beta.2",
|
||||
"stylelint": "^16.9.0",
|
||||
"stylelint-config-recommended": "^14.0.1",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"stylelint-scss": "^6.5.1",
|
||||
"swiper": "^11.1.12",
|
||||
"terracotta": "^1.0.5",
|
||||
"stylelint-scss": "^6.6.0",
|
||||
"swiper": "^11.1.14",
|
||||
"terracotta": "^1.0.6",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tslib": "^2.7.0",
|
||||
"typescript": "^5.5.4",
|
||||
"typescript": "^5.6.2",
|
||||
"typograf": "^7.4.1",
|
||||
"uniqolor": "^1.1.1",
|
||||
"vinxi": "^0.4.2",
|
||||
"vinxi": "^0.4.3",
|
||||
"vite-plugin-mkcert": "^1.17.6",
|
||||
"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",
|
||||
"yjs": "13.6.18"
|
||||
"yjs": "13.6.19"
|
||||
},
|
||||
"overrides": {
|
||||
"yjs": "13.6.18",
|
||||
"yjs": "13.6.19",
|
||||
"y-prosemirror": "1.2.12",
|
||||
"prosemirror-view": "1.34.2"
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Router } from '@solidjs/router'
|
|||
import { FileRoutes } from '@solidjs/start/router'
|
||||
import { type JSX, Suspense } from 'solid-js'
|
||||
|
||||
import { AuthToken } from '@authorizerdev/authorizer-js'
|
||||
import { Loading } from './components/_shared/Loading'
|
||||
import { AuthorsProvider } from './context/authors'
|
||||
import { EditorProvider } from './context/editor'
|
||||
|
@ -10,9 +11,9 @@ import { FeedProvider } from './context/feed'
|
|||
import { LocalizeProvider } from './context/localize'
|
||||
import { SessionProvider } from './context/session'
|
||||
import { TopicsProvider } from './context/topics'
|
||||
import { UIProvider } from './context/ui' // snackbar included
|
||||
import { UIProvider } from './context/ui'
|
||||
|
||||
import '~/styles/app.scss'
|
||||
import { AuthToken } from '@authorizerdev/authorizer-js'
|
||||
|
||||
export const Providers = (props: { children?: JSX.Element }) => {
|
||||
const sessionStateChanged = (payload: AuthToken) => {
|
||||
|
|
|
@ -74,7 +74,7 @@ export const PlayerHeader = (props: Props) => {
|
|||
onChange={({ target }) => props.onVolumeChange(Number(target.value))}
|
||||
/>
|
||||
</Show>
|
||||
<button onClick={toggleVolumeBar} class={styles.volumeButton} role="button" aria-label="Volume">
|
||||
<button onClick={toggleVolumeBar} class={styles.volumeButton} aria-label="Volume">
|
||||
<Icon name="volume" />
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -84,7 +84,6 @@ export const CommentRatingControl = (props: Props) => {
|
|||
return (
|
||||
<div class={styles.commentRating}>
|
||||
<button
|
||||
role="button"
|
||||
disabled={!(canVote() && uid())}
|
||||
onClick={() => handleRatingChange(true)}
|
||||
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
|
||||
|
@ -110,7 +109,6 @@ export const CommentRatingControl = (props: Props) => {
|
|||
/>
|
||||
</Popup>
|
||||
<button
|
||||
role="button"
|
||||
disabled={!(canVote() && uid())}
|
||||
onClick={() => handleRatingChange(false)}
|
||||
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
|
||||
|
|
|
@ -112,7 +112,7 @@ export const FullArticle = (props: Props) => {
|
|||
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)
|
||||
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 props.article.topics?.[0]
|
||||
|
@ -319,7 +319,7 @@ export const FullArticle = (props: Props) => {
|
|||
|
||||
const shareUrl = createMemo(() => getShareUrl({ pathname: `/${props.article.slug || ''}` }))
|
||||
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 (
|
||||
<>
|
||||
<For each={imageUrls()}>{(imageUrl) => <Link rel="preload" as="image" href={imageUrl} />}</For>
|
||||
|
|
|
@ -77,17 +77,15 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
|
|||
}
|
||||
|
||||
createEffect(() => {
|
||||
switch (selectedMenuItem()) {
|
||||
case 'image': {
|
||||
if (selectedMenuItem() === 'image') {
|
||||
showModal('uploadImage')
|
||||
return
|
||||
}
|
||||
case 'horizontal-rule': {
|
||||
if (selectedMenuItem() === 'horizontal-rule') {
|
||||
props.editor?.chain().focus().setHorizontalRule().run()
|
||||
setSelectedMenuItem()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const closeUploadModalHandler = () => {
|
||||
|
|
|
@ -38,7 +38,7 @@ export const Figure = Node.create({
|
|||
}
|
||||
const img = node.querySelector('img')
|
||||
const iframe = node.querySelector('iframe')
|
||||
let dataType = null
|
||||
let dataType: string | undefined
|
||||
if (img) {
|
||||
dataType = 'image'
|
||||
} else if (iframe) {
|
||||
|
|
|
@ -8,15 +8,17 @@ import type { Topic } from '~/graphql/schema/core.gen'
|
|||
import { getRandomItemsFromArray } from '~/utils/random'
|
||||
import styles from './TopicsNav.module.scss'
|
||||
|
||||
const russianChars = /[ЁА-яё]/
|
||||
|
||||
export const RandomTopics = () => {
|
||||
const { sortedTopics } = useTopics()
|
||||
const { lang, t } = useLocalize()
|
||||
const tag = (topic: Topic) =>
|
||||
/[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
|
||||
russianChars.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
|
||||
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||
createEffect(
|
||||
on(sortedTopics, (ttt: Topic[]) => {
|
||||
if (ttt?.length) {
|
||||
if (ttt?.length > 0) {
|
||||
setRandomTopics(getRandomItemsFromArray(ttt))
|
||||
}
|
||||
})
|
||||
|
|
|
@ -21,6 +21,9 @@ export const ABC = {
|
|||
en: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'
|
||||
}
|
||||
|
||||
const russianChars = /[^ËА-яё]/
|
||||
const latinChars = /[^A-z]/
|
||||
|
||||
export const AllTopics = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
const alphabet = createMemo(() => ABC[lang()])
|
||||
|
@ -35,8 +38,8 @@ export const AllTopics = (props: Props) => {
|
|||
return topics().reduce(
|
||||
(acc, topic) => {
|
||||
let letter = lang() === 'en' ? topic.slug[0].toUpperCase() : (topic?.title?.[0] || '').toUpperCase()
|
||||
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '#'
|
||||
if (/[^A-z]/.test(letter) && lang() === 'en') letter = '#'
|
||||
if (russianChars.test(letter) && lang() === 'ru') letter = '#'
|
||||
if (latinChars.test(letter) && lang() === 'en') letter = '#'
|
||||
if (!acc[letter]) acc[letter] = []
|
||||
acc[letter].push(topic)
|
||||
return acc
|
||||
|
|
|
@ -130,7 +130,7 @@ export const EditView = (props: Props) => {
|
|||
draft,
|
||||
(d) => {
|
||||
if (d) {
|
||||
const draftForm = Object.keys(d).length !== 0 ? d : { shoutId: props.shout.id }
|
||||
const draftForm = Object.keys(d) ? d : { shoutId: props.shout.id }
|
||||
setForm(draftForm)
|
||||
console.debug('draft from localstorage: ', draftForm)
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ export const FeedView = (props: FeedProps) => {
|
|||
<Placeholder type={loc?.pathname} mode="feed" />
|
||||
</Show>
|
||||
|
||||
<Show when={(session() || loc?.pathname === 'feed') && props.shouts?.length}>
|
||||
<Show when={(session() || loc?.pathname === 'feed') && props.shouts}>
|
||||
<div class={styles.filtersContainer}>
|
||||
<ul class={clsx('view-switcher', styles.feedFilter)}>
|
||||
<li class={clsx({ 'view-switcher__item--selected': !props.order })}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useNavigate } from '@solidjs/router'
|
||||
import { clsx } from 'clsx'
|
||||
import { Show, createEffect, createMemo, createSignal, lazy, onMount } from 'solid-js'
|
||||
import { Show, createEffect, createSignal, lazy, onMount } from 'solid-js'
|
||||
import { createStore } from 'solid-js/store'
|
||||
import { Button } from '~/components/_shared/Button'
|
||||
import { Icon } from '~/components/_shared/Icon'
|
||||
|
@ -76,7 +76,7 @@ export const PublishSettings = (props: Props) => {
|
|||
return props.form.description
|
||||
}
|
||||
|
||||
const initialData = createMemo(() => {
|
||||
const initialData = () => {
|
||||
return {
|
||||
coverImageUrl: props.form?.coverImageUrl,
|
||||
mainTopic: props.form?.mainTopic || EMPTY_TOPIC,
|
||||
|
@ -86,7 +86,7 @@ export const PublishSettings = (props: Props) => {
|
|||
description: composeDescription() || '',
|
||||
selectedTopics: []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const [settingsForm, setSettingsForm] = createStore<FormConfig>(emptyConfig)
|
||||
|
||||
|
@ -239,8 +239,16 @@ export const PublishSettings = (props: Props) => {
|
|||
|
||||
<h4>{t('Slug')}</h4>
|
||||
<div class="pretty-form__item">
|
||||
<input type="text" name="slug" id="slug" value={settingsForm.slug} onInput={removeSpecial} />
|
||||
<label for="slug">{t('Slug')}</label>
|
||||
<label for="slug">
|
||||
<input
|
||||
type="text"
|
||||
name="slug"
|
||||
id="slug"
|
||||
value={settingsForm.slug}
|
||||
onInput={removeSpecial}
|
||||
/>
|
||||
{t('Slug')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<h4>{t('Topics')}</h4>
|
||||
|
|
|
@ -37,14 +37,13 @@ export const DropArea = (props: Props) => {
|
|||
const runUpload = async (files: UploadFile[]) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
|
||||
const results = []
|
||||
for (const file of files) {
|
||||
const handler = props.fileType === 'image' ? handleImageUpload : handleFileUpload
|
||||
const result = await handler(file, session()?.access_token as string)
|
||||
results.push(result)
|
||||
}
|
||||
props.onUpload(results)
|
||||
const tkn = session()?.access_token as string
|
||||
// Since handler returns a promise, we need to await the results
|
||||
tkn &&
|
||||
Promise.all(files.map((file) => handler(file, tkn)))
|
||||
.then(props.onUpload)
|
||||
.catch(console.error)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
setLoading(false)
|
||||
|
|
|
@ -61,7 +61,7 @@ export const Popover = (props: Props) => {
|
|||
<>
|
||||
{props.children(setAnchor)}
|
||||
<Show when={show() && !props.disabled}>
|
||||
<div ref={setPopper} class={styles.tooltip} role="tooltip">
|
||||
<div ref={setPopper} class={styles.tooltip}>
|
||||
{props.content}
|
||||
<div class={styles.arrow} data-popper-arrow={true} />
|
||||
</div>
|
||||
|
|
|
@ -55,7 +55,6 @@ export const ShareLinks = (props: Props) => {
|
|||
<ul class="nodash">
|
||||
<li>
|
||||
<button
|
||||
role="button"
|
||||
class={clsx(styles.shareControl, popupStyles.action)}
|
||||
onClick={() => handleShare(FACEBOOK)}
|
||||
>
|
||||
|
@ -65,7 +64,6 @@ export const ShareLinks = (props: Props) => {
|
|||
</li>
|
||||
<li>
|
||||
<button
|
||||
role="button"
|
||||
class={clsx(styles.shareControl, popupStyles.action)}
|
||||
onClick={() => handleShare(TWITTER)}
|
||||
>
|
||||
|
@ -75,7 +73,6 @@ export const ShareLinks = (props: Props) => {
|
|||
</li>
|
||||
<li>
|
||||
<button
|
||||
role="button"
|
||||
class={clsx(styles.shareControl, popupStyles.action)}
|
||||
onClick={() => handleShare(TELEGRAM)}
|
||||
>
|
||||
|
@ -84,11 +81,7 @@ export const ShareLinks = (props: Props) => {
|
|||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
role="button"
|
||||
class={clsx(styles.shareControl, popupStyles.action)}
|
||||
onClick={() => handleShare(VK)}
|
||||
>
|
||||
<button class={clsx(styles.shareControl, popupStyles.action)} onClick={() => handleShare(VK)}>
|
||||
<Icon name="vk-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||
VK
|
||||
</button>
|
||||
|
@ -97,19 +90,17 @@ export const ShareLinks = (props: Props) => {
|
|||
<Show
|
||||
when={props.variant === 'inModal'}
|
||||
fallback={
|
||||
<button
|
||||
role="button"
|
||||
class={clsx(styles.shareControl, popupStyles.action)}
|
||||
onClick={copyLink}
|
||||
>
|
||||
<button class={clsx(styles.shareControl, popupStyles.action)} onClick={copyLink}>
|
||||
<Icon name="link-white" class={clsx(styles.icon, popupStyles.icon)} />
|
||||
{t('Copy link')}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<form class={clsx('pretty-form__item', styles.linkInput)}>
|
||||
<label for="link">
|
||||
<input type="text" name="link" readonly value={props.shareUrl} />
|
||||
<label for="link">{t('Copy link')}</label>
|
||||
{t('Copy link')}
|
||||
</label>
|
||||
|
||||
<Popover content={t('Copy link')}>
|
||||
{(triggerRef: (el: HTMLElement) => void) => (
|
||||
|
|
|
@ -16,6 +16,9 @@ type Props = {
|
|||
onVideoDelete?: () => void
|
||||
articleView?: boolean
|
||||
}
|
||||
const watchPattern = /watch=(\w+)/
|
||||
const ytPattern = /youtu.be\/(\w+)/
|
||||
const vimeoPattern = /vimeo.com\/(\d+)/
|
||||
|
||||
export const VideoPlayer = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
|
@ -27,14 +30,14 @@ export const VideoPlayer = (props: Props) => {
|
|||
setIsVimeo(!isYoutube)
|
||||
if (isYoutube) {
|
||||
if (props.videoUrl.includes('youtube.com')) {
|
||||
const videoIdMatch = props.videoUrl.match(/watch=(\w+)/)
|
||||
const videoIdMatch = props.videoUrl.match(watchPattern)
|
||||
setVideoId(videoIdMatch?.[1])
|
||||
} else {
|
||||
const videoIdMatch = props.videoUrl.match(/youtu.be\/(\w+)/)
|
||||
const videoIdMatch = props.videoUrl.match(ytPattern)
|
||||
setVideoId(videoIdMatch?.[1])
|
||||
}
|
||||
} else {
|
||||
const videoIdMatch = props.videoUrl.match(/vimeo.com\/(\d+)/)
|
||||
const videoIdMatch = props.videoUrl.match(vimeoPattern)
|
||||
setVideoId(videoIdMatch?.[1])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -2,15 +2,21 @@ import { Author } from '~/graphql/schema/core.gen'
|
|||
import { capitalize } from '~/utils/capitalize'
|
||||
import { translit } from './translit'
|
||||
|
||||
export const isCyrillic = (s: string): boolean => {
|
||||
const cyrillicRegex = /[\u0400-\u04FF]/ // Range for Cyrillic characters
|
||||
const allChars = /[^\dA-zА-я]/
|
||||
const rusChars = /[^ËА-яё]/
|
||||
const enChars = /[^A-z]/
|
||||
|
||||
export const isCyrillic = (s: string): boolean => {
|
||||
return cyrillicRegex.test(s)
|
||||
}
|
||||
|
||||
export const translateAuthor = (author: Author, lng: string) =>
|
||||
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
|
||||
|
||||
export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Author, lng: string) => {
|
||||
|
@ -18,7 +24,7 @@ export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Autho
|
|||
if (!letter && author && author.name) {
|
||||
const name =
|
||||
translateAuthor(author, lng || 'ru')
|
||||
?.replace(/[^\dA-zА-я]/, ' ')
|
||||
?.replace(allChars, ' ')
|
||||
.trim() || ''
|
||||
const nameParts = name.trim().split(' ')
|
||||
const found = nameParts.filter(Boolean).pop()
|
||||
|
@ -26,8 +32,8 @@ export const authorLetterReduce = (acc: { [x: string]: Author[] }, author: Autho
|
|||
letter = found[0].toUpperCase()
|
||||
}
|
||||
}
|
||||
if (/[^ËА-яё]/.test(letter) && lng === 'ru') letter = '@'
|
||||
if (/[^A-z]/.test(letter) && lng === 'en') letter = '@'
|
||||
if (rusChars.test(letter) && lng === 'ru') letter = '@'
|
||||
if (enChars.test(letter) && lng === 'en') letter = '@'
|
||||
|
||||
if (!acc[letter]) acc[letter] = []
|
||||
author.name = translateAuthor(author, lng)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import translitConfig from './abc-translit.json'
|
||||
|
||||
const ru2en: { [key: string]: string } = translitConfig
|
||||
|
||||
const rusChars = /[ЁА-яё]/
|
||||
export const translit = (str: string) => {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const isCyrillic = /[ЁА-яё]/.test(str)
|
||||
const isCyrillic = rusChars.test(str)
|
||||
|
||||
if (!isCyrillic) {
|
||||
return str
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
const audioExts = /\.(wav|flac|mp3|aac|jpg|jpeg|png|gif)$/i
|
||||
|
||||
const removeMediaFileExtension = (fileName: string) => {
|
||||
return fileName.replace(/\.(wav|flac|mp3|aac|jpg|jpeg|png|gif)$/i, '')
|
||||
return fileName.replace(audioExts, '')
|
||||
}
|
||||
|
||||
export const composeMediaItems = (
|
||||
|
|
19
src/lib/fromPeriod.ts
Normal file
19
src/lib/fromPeriod.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
export type FromPeriod = 'week' | 'month' | 'year'
|
||||
|
||||
export const getFromDate = (period: FromPeriod): number => {
|
||||
const now = new Date()
|
||||
let d: Date = now
|
||||
switch (period) {
|
||||
case 'month': {
|
||||
d = new Date(now.setMonth(now.getMonth() - 1))
|
||||
break
|
||||
}
|
||||
case 'year': {
|
||||
d = new Date(now.setFullYear(now.getFullYear() - 1))
|
||||
break
|
||||
}
|
||||
default: // 'week'
|
||||
d = new Date(now.setDate(now.getDate() - 7))
|
||||
}
|
||||
return Math.floor(d.getTime() / 1000)
|
||||
}
|
|
@ -11,37 +11,16 @@ import { ReactionsProvider } from '~/context/reactions'
|
|||
import { useTopics } from '~/context/topics'
|
||||
import { loadShouts } from '~/graphql/api/public'
|
||||
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||
import { FromPeriod, getFromDate } from '~/lib/fromPeriod'
|
||||
import { SHOUTS_PER_PAGE } from '../(main)'
|
||||
|
||||
export type FeedPeriod = 'week' | 'month' | 'year'
|
||||
|
||||
export type PeriodItem = {
|
||||
value: FeedPeriod
|
||||
value: FromPeriod
|
||||
title: string
|
||||
}
|
||||
|
||||
export type FeedSearchParams = {
|
||||
period: FeedPeriod
|
||||
}
|
||||
|
||||
const getFromDate = (period: FeedPeriod): number => {
|
||||
const now = new Date()
|
||||
let d: Date = now
|
||||
switch (period) {
|
||||
case 'week': {
|
||||
d = new Date(now.setDate(now.getDate() - 7))
|
||||
break
|
||||
}
|
||||
case 'month': {
|
||||
d = new Date(now.setMonth(now.getMonth() - 1))
|
||||
break
|
||||
}
|
||||
case 'year': {
|
||||
d = new Date(now.setFullYear(now.getFullYear() - 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
return Math.floor(d.getTime() / 1000)
|
||||
period: FromPeriod
|
||||
}
|
||||
|
||||
const feedLoader = async (options: Partial<LoadShoutsOptions>, _client?: Client) => {
|
||||
|
@ -57,6 +36,8 @@ export const route = {
|
|||
}
|
||||
}
|
||||
|
||||
const paramPattern = /^(hot|likes)$/
|
||||
|
||||
export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => {
|
||||
const [searchParams] = useSearchParams<FeedSearchParams>() // ?period=month
|
||||
const { t } = useLocalize()
|
||||
|
@ -71,7 +52,6 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
|
|||
// load more feed
|
||||
const loadMoreFeed = async (offset?: number) => {
|
||||
// /feed/:order: - select order setting
|
||||
const paramPattern = /^(hot|likes)$/
|
||||
const order =
|
||||
(props.params.order && paramPattern.test(props.params.order)
|
||||
? props.params.order === 'hot'
|
||||
|
@ -88,7 +68,7 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
|
|||
// ?period=month - time period filter
|
||||
if (searchParams?.period) {
|
||||
const period = searchParams?.period || 'month'
|
||||
options.filters = { after: getFromDate(period as FeedPeriod) }
|
||||
options.filters = { after: getFromDate(period as FromPeriod) }
|
||||
}
|
||||
|
||||
const loaded = await feedLoader(options)
|
||||
|
@ -107,9 +87,8 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
|
|||
})
|
||||
|
||||
const order = createMemo(() => {
|
||||
const paramOrderPattern = /^(hot|likes)$/
|
||||
return (
|
||||
(paramOrderPattern.test(props.params.order)
|
||||
(paramPattern.test(props.params.order)
|
||||
? props.params.order === 'hot'
|
||||
? 'last_comment'
|
||||
: props.params.order
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from '~/graphql/api/private'
|
||||
import { graphqlClientCreate } from '~/graphql/client'
|
||||
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||
import { FromPeriod, getFromDate } from '~/lib/fromPeriod'
|
||||
|
||||
const feeds = {
|
||||
followed: loadFollowedShouts,
|
||||
|
@ -26,32 +27,13 @@ const feeds = {
|
|||
coauthored: loadCoauthoredShouts,
|
||||
unrated: loadUnratedShouts
|
||||
}
|
||||
|
||||
export type FeedPeriod = 'week' | 'month' | 'year'
|
||||
export type FeedSearchParams = { period?: FeedPeriod }
|
||||
|
||||
const getFromDate = (period: FeedPeriod): number => {
|
||||
const now = new Date()
|
||||
let d: Date = now
|
||||
switch (period) {
|
||||
case 'week': {
|
||||
d = new Date(now.setDate(now.getDate() - 7))
|
||||
break
|
||||
}
|
||||
case 'month': {
|
||||
d = new Date(now.setMonth(now.getMonth() - 1))
|
||||
break
|
||||
}
|
||||
case 'year': {
|
||||
d = new Date(now.setFullYear(now.getFullYear() - 1))
|
||||
break
|
||||
}
|
||||
}
|
||||
return Math.floor(d.getTime() / 1000)
|
||||
}
|
||||
export type FeedSearchParams = { period?: FromPeriod }
|
||||
|
||||
// /feed/my/followed/hot
|
||||
|
||||
const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/
|
||||
const paramOrderPattern = /^(hot|likes)$/
|
||||
|
||||
export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => {
|
||||
const [searchParams] = useSearchParams<FeedSearchParams>() // ?period=month
|
||||
const { t } = useLocalize()
|
||||
|
@ -67,12 +49,10 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
|
|||
|
||||
// /feed/my/:mode:
|
||||
const mode = createMemo(() => {
|
||||
const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/
|
||||
return props.params.mode && paramModePattern.test(props.params.mode) ? props.params.mode : 'followed'
|
||||
})
|
||||
|
||||
const order = createMemo(() => {
|
||||
const paramOrderPattern = /^(hot|likes)$/
|
||||
return (
|
||||
(paramOrderPattern.test(props.params.order)
|
||||
? props.params.order === 'hot'
|
||||
|
@ -96,7 +76,7 @@ export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>)
|
|||
// ?period=month - time period filter
|
||||
if (searchParams?.period) {
|
||||
const period = searchParams?.period || 'month'
|
||||
options.filters = { after: getFromDate(period as FeedPeriod) }
|
||||
options.filters = { after: getFromDate(period as FromPeriod) }
|
||||
}
|
||||
|
||||
const shoutsLoader = gqlHandler(client(), options)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
const emailPattern = /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i
|
||||
|
||||
export const validateEmail = (email: string) => {
|
||||
if (!email) return false
|
||||
|
||||
return /^[\w%+.-]+@[\d.a-z-]+\.[a-z]{2,}$/i.test(email)
|
||||
return emailPattern.test(email)
|
||||
}
|
||||
|
||||
export const validateUrl = (value: string) => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { type Page, expect, test } from '@playwright/test'
|
|||
/* Global starting test config */
|
||||
|
||||
let page: Page
|
||||
|
||||
const discoursPattern = /Дискурс/
|
||||
function httpsGet(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
|
@ -50,7 +50,7 @@ test.beforeAll(async ({ browser }) => {
|
|||
page = await browser.newPage()
|
||||
test.setTimeout(150000)
|
||||
await page.goto(baseURL)
|
||||
await expect(page).toHaveTitle(/Дискурс/)
|
||||
await expect(page).toHaveTitle(discoursPattern)
|
||||
console.log('Localhost server started successfully!')
|
||||
})
|
||||
test.afterAll(async () => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import { type Page, expect, test } from '@playwright/test'
|
|||
/* Global starting test config */
|
||||
|
||||
let page: Page
|
||||
|
||||
const discoursPattern = /Дискурс/
|
||||
function httpsGet(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
|
@ -50,7 +50,7 @@ test.beforeAll(async ({ browser }) => {
|
|||
page = await browser.newPage()
|
||||
test.setTimeout(150000)
|
||||
await page.goto(baseURL)
|
||||
await expect(page).toHaveTitle(/Дискурс/)
|
||||
await expect(page).toHaveTitle(discoursPattern)
|
||||
await page.getByRole('link', { name: 'Войти' }).click()
|
||||
console.log('Localhost server started successfully!')
|
||||
await page.close()
|
||||
|
|
|
@ -6,6 +6,8 @@ import { type Page, expect, test } from '@playwright/test'
|
|||
|
||||
let page: Page
|
||||
|
||||
const discoursPattern = /Дискурс/
|
||||
|
||||
function httpsGet(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
|
@ -50,7 +52,7 @@ test.beforeAll(async ({ browser }) => {
|
|||
page = await browser.newPage()
|
||||
test.setTimeout(150000)
|
||||
await page.goto(baseURL)
|
||||
await expect(page).toHaveTitle(/Дискурс/)
|
||||
await expect(page).toHaveTitle(discoursPattern)
|
||||
console.log('Localhost server started successfully!')
|
||||
await page.close()
|
||||
})
|
||||
|
|
|
@ -8,6 +8,8 @@ let page: Page
|
|||
|
||||
/* Global starting test config */
|
||||
|
||||
const discoursPattern = /Дискурс/
|
||||
|
||||
function httpsGet(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
https
|
||||
|
@ -54,7 +56,7 @@ test.beforeAll(async ({ browser }) => {
|
|||
page = await context.newPage()
|
||||
test.setTimeout(150000)
|
||||
await page.goto(baseURL)
|
||||
await expect(page).toHaveTitle(/Дискурс/)
|
||||
await expect(page).toHaveTitle(discoursPattern)
|
||||
await page.getByRole('link', { name: 'Войти' }).click()
|
||||
console.log('Localhost server started successfully!')
|
||||
await page.close()
|
||||
|
|
Loading…
Reference in New Issue
Block a user