revised changes

This commit is contained in:
Untone 2024-05-01 17:33:37 +03:00
parent 04978ebc7c
commit 98e0bb1078
31 changed files with 1290 additions and 1230 deletions

View File

@ -1,8 +1,21 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"files": { "files": {
"include": ["*.tsx", "*.ts", "*.js", "*.json"], "include": [
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen", "*.gen.ts", "*.d.ts"] "*.tsx",
"*.ts",
"*.js",
"*.json"
],
"ignore": [
"./dist",
"./node_modules",
".husky",
"docs",
"gen",
"*.gen.ts",
"*.d.ts"
]
}, },
"vcs": { "vcs": {
"defaultBranch": "dev", "defaultBranch": "dev",
@ -10,13 +23,19 @@
}, },
"organizeImports": { "organizeImports": {
"enabled": true, "enabled": true,
"ignore": ["./api", "./gen"] "ignore": [
"./api",
"./gen"
]
}, },
"formatter": { "formatter": {
"indentStyle": "space", "indentStyle": "space",
"indentWidth": 2, "indentWidth": 2,
"lineWidth": 108, "lineWidth": 108,
"ignore": ["./src/graphql/schema", "./gen"] "ignore": [
"./src/graphql/schema",
"./gen"
]
}, },
"javascript": { "javascript": {
"formatter": { "formatter": {
@ -29,14 +48,21 @@
} }
}, },
"linter": { "linter": {
"ignore": ["*.scss", "*.md", ".DS_Store", "*.svg", "*.d.ts"], "ignore": [
"*.scss",
"*.md",
".DS_Store",
"*.svg",
"*.d.ts"
],
"enabled": true, "enabled": true,
"rules": { "rules": {
"all": true, "all": true,
"complexity": { "complexity": {
"noForEach": "off", "noForEach": "off",
"useOptionalChain": "warn", "useOptionalChain": "warn",
"useLiteralKeys": "off" "useLiteralKeys": "off",
"noExcessiveCognitiveComplexity": "off"
}, },
"correctness": { "correctness": {
"useHookAtTopLevel": "off" "useHookAtTopLevel": "off"
@ -54,15 +80,18 @@
"noSvgWithoutTitle": "off" "noSvgWithoutTitle": "off"
}, },
"nursery": { "nursery": {
"useImportRestrictions": "off", "useImportRestrictions": "off"
"useImportType": "off", },
"useFilenamingConvention": "off" "performance": {
"noBarrelFile": "off"
}, },
"style": { "style": {
"useBlockStatements": "off", "useBlockStatements": "off",
"noImplicitBoolean": "off", "noImplicitBoolean": "off",
"useNamingConvention": "off", "useNamingConvention": "off",
"noDefaultExport": "off" "useImportType": "off",
"noDefaultExport": "off",
"useFilenamingConvention": "off"
}, },
"suspicious": { "suspicious": {
"noConsoleLog": "off", "noConsoleLog": "off",
@ -70,4 +99,4 @@
} }
} }
} }
} }

2151
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -11,12 +11,12 @@
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel", "deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
"dev": "vite", "dev": "vite",
"e2e": "npx playwright test --project=chromium", "e2e": "npx playwright test --project=chromium",
"fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix", "fix": "npm run lint:code:fix && stylelint **/*.{scss,css} --fix",
"format": "npx @biomejs/biome format src/. --write", "format": "npx @biomejs/biome format src/. --write",
"hygen": "HYGEN_TMPLS=gen hygen", "hygen": "HYGEN_TMPLS=gen hygen",
"postinstall": "npm run codegen && npx patch-package", "postinstall": "npm run codegen && npx patch-package",
"check:code": "npx @biomejs/biome check src --log-kind=compact --verbose", "check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
"check:code:fix": "npx @biomejs/biome check src --log-kind=compact", "check:code:fix": "npx @biomejs/biome lint src --log-kind=compact",
"lint": "npm run lint:code && stylelint **/*.{scss,css}", "lint": "npm run lint:code && stylelint **/*.{scss,css}",
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose", "lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose", "lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
@ -28,6 +28,7 @@
"typecheck:watch": "tsc --noEmit --watch" "typecheck:watch": "tsc --noEmit --watch"
}, },
"dependencies": { "dependencies": {
"buffer": "6.0.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"idb": "8.0.0", "idb": "8.0.0",
"mailgun.js": "10.1.0" "mailgun.js": "10.1.0"
@ -35,7 +36,7 @@
"devDependencies": { "devDependencies": {
"@authorizerdev/authorizer-js": "2.0.0", "@authorizerdev/authorizer-js": "2.0.0",
"@babel/core": "7.23.3", "@babel/core": "7.23.3",
"@biomejs/biome": "^1.5.3", "@biomejs/biome": "^1.7.2",
"@graphql-codegen/cli": "^5.0.0", "@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1", "@graphql-codegen/typescript-operations": "^4.0.1",
@ -129,7 +130,7 @@
"typograf": "7.3.0", "typograf": "7.3.0",
"uniqolor": "1.1.0", "uniqolor": "1.1.0",
"vike": "0.4.148", "vike": "0.4.148",
"vite": "5.1.2", "vite": "5.2.10",
"vite-plugin-mkcert": "^1.17.3", "vite-plugin-mkcert": "^1.17.3",
"vite-plugin-sass-dts": "^1.3.17", "vite-plugin-sass-dts": "^1.3.17",
"vite-plugin-solid": "2.10.1", "vite-plugin-solid": "2.10.1",
@ -139,5 +140,8 @@
"overrides": { "overrides": {
"y-prosemirror": "1.2.2", "y-prosemirror": "1.2.2",
"yjs": "13.6.12" "yjs": "13.6.12"
} },
} "trustedDependencies": [
"@biomejs/biome"
]
}

View File

@ -14,7 +14,7 @@ type Props = {
} }
export const CommentDate = (props: Props) => { export const CommentDate = (props: Props) => {
const { t, formatDate } = useLocalize() const { formatDate } = useLocalize()
const formattedDate = (date: number) => { const formattedDate = (date: number) => {
const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort const formatDateOptions: Intl.DateTimeFormatOptions = props.isShort

View File

@ -36,7 +36,7 @@ export const AuthorBadge = (props: Props) => {
const [isSubscribed, setIsSubscribed] = createSignal<boolean>() const [isSubscribed, setIsSubscribed] = createSignal<boolean>()
createEffect(() => { createEffect(() => {
if (!subscriptions || !props.author) return if (!(subscriptions && props.author)) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id) const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed) setIsSubscribed(subscribed)
}) })
@ -45,7 +45,7 @@ export const AuthorBadge = (props: Props) => {
setIsMobileView(!mediaMatches.sm) setIsMobileView(!mediaMatches.sm)
}) })
const { setFollowing } = useFollowing() // const { setFollowing } = useFollowing()
const { changeSearchParams } = useRouter() const { changeSearchParams } = useRouter()
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
@ -119,6 +119,9 @@ export const AuthorBadge = (props: Props) => {
<Show when={props.author?.stat.shouts > 0}> <Show when={props.author?.stat.shouts > 0}>
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div> <div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
</Show> </Show>
<Show when={props.author?.stat.comments > 0}>
<div>{t('CommentsWithCount', { count: props.author.stat?.comments ?? 0 })}</div>
</Show>
<Show when={props.author?.stat.followers > 0}> <Show when={props.author?.stat.followers > 0}>
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div> <div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
</Show> </Show>

View File

@ -43,7 +43,7 @@ export const AuthorCard = (props: Props) => {
}) })
createEffect(() => { createEffect(() => {
if (!subscriptions || !props.author) return if (!(subscriptions && props.author)) return
const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id) const subscribed = subscriptions.authors?.some((authorEntity) => authorEntity.id === props.author?.id)
setIsSubscribed(subscribed) setIsSubscribed(subscribed)
}) })

View File

@ -1,9 +1,7 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal, on, onMount } from 'solid-js' import { For, Show, createEffect, createSignal, on } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { apiClient } from '../../graphql/client/core' import { apiClient } from '../../graphql/client/core'
import { Author } from '../../graphql/schema/core.gen'
import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors' import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { InlineLoader } from '../InlineLoader' import { InlineLoader } from '../InlineLoader'

View File

@ -1,7 +1,7 @@
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types' import type { AuthModalSearchParams } from '../Nav/AuthModal/types'
import styles from './Hero.module.scss' import styles from './Hero.module.scss'

View File

@ -1,5 +1,3 @@
import type { Doc } from 'yjs/dist/src/utils/Doc'
import { HocuspocusProvider } from '@hocuspocus/provider' import { HocuspocusProvider } from '@hocuspocus/provider'
import { isTextSelection } from '@tiptap/core' import { isTextSelection } from '@tiptap/core'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
@ -30,7 +28,7 @@ import { Underline } from '@tiptap/extension-underline'
import { createEffect, createSignal, onCleanup } from 'solid-js' import { createEffect, createSignal, onCleanup } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap' import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import uniqolor from 'uniqolor' import uniqolor from 'uniqolor'
import * as Y from 'yjs' import { Doc } from 'yjs'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
@ -85,7 +83,7 @@ export const Editor = (props: Props) => {
const docName = `shout-${props.shoutId}` const docName = `shout-${props.shoutId}`
if (!yDocs[docName]) { if (!yDocs[docName]) {
yDocs[docName] = new Y.Doc() yDocs[docName] = new Doc()
} }
if (!providers[docName]) { if (!providers[docName]) {

View File

@ -29,8 +29,10 @@ const embedData = (data) => {
const result: { src: string; width?: string; height?: string } = { src: '' } const result: { src: string; width?: string; height?: string } = { src: '' }
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
const attribute = attributes[i] const attribute = attributes.item(i)
result[attribute.name] = attribute.value if (attribute) {
result[attribute.name] = attribute.value
}
} }
return result return result

View File

@ -13,8 +13,6 @@ import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { DarkModeToggle } from '../../_shared/DarkModeToggle' import { DarkModeToggle } from '../../_shared/DarkModeToggle'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { useSnackbar } from '../../../context/snackbar'
import styles from './Panel.module.scss' import styles from './Panel.module.scss'
const typograf = new Typograf({ locale: ['ru', 'en-US'] }) const typograf = new Typograf({ locale: ['ru', 'en-US'] })

View File

@ -4,8 +4,6 @@ import type { Author, Shout, Topic } from '../../graphql/schema/core.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'
import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'

View File

@ -1,4 +1,3 @@
import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Loading } from '../_shared/Loading' import { Loading } from '../_shared/Loading'
import styles from './InlineLoader.module.scss' import styles from './InlineLoader.module.scss'
@ -7,7 +6,7 @@ type Props = {
class?: string class?: string
} }
export const InlineLoader = (props: Props) => { export const InlineLoader = (_props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
return ( return (
<div class={styles.InlineLoader}> <div class={styles.InlineLoader}>

View File

@ -116,7 +116,7 @@ export const RegisterForm = () => {
const handleCheckEmailStatus = (status: EmailStatus | string) => { const handleCheckEmailStatus = (status: EmailStatus | string) => {
switch (status) { switch (status) {
case 'not verified': case 'not verified': {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
email: ( email: (
@ -129,8 +129,9 @@ export const RegisterForm = () => {
), ),
})) }))
break break
case 'verified': }
setValidationErrors((prev) => ({ case 'verified': {
setValidationErrors((_prev) => ({
email: ( email: (
<> <>
{t('This email is registered')}. {t('try')} {t('This email is registered')}. {t('try')}
@ -142,7 +143,8 @@ export const RegisterForm = () => {
), ),
})) }))
break break
case 'registered': }
case 'registered': {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
email: ( email: (
@ -156,9 +158,11 @@ export const RegisterForm = () => {
), ),
})) }))
break break
default: }
default: {
console.info('[RegisterForm] email is not registered') console.info('[RegisterForm] email is not registered')
break break
}
} }
} }

View File

@ -42,14 +42,16 @@ export const TopicCard = (props: TopicProps) => {
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing() const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
createEffect(() => { createEffect(() => {
if (!subscriptions || !props.topic) return if (!(subscriptions && props.topic)) return
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id) const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
setIsSubscribed(subscribed) setIsSubscribed(subscribed)
}) })
const handleFollowClick = () => { const handleFollowClick = () => {
requireAuthentication(() => { requireAuthentication(() => {
follow(FollowingEntity.Topic, props.topic.slug) isSubscribed()
? unfollow(FollowingEntity.Topic, props.topic.slug)
: follow(FollowingEntity.Topic, props.topic.slug)
}, 'subscribe') }, 'subscribe')
} }

View File

@ -26,7 +26,7 @@ export const TopicBadge = (props: Props) => {
const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing() const { follow, unfollow, subscriptions, subscribeInAction } = useFollowing()
createEffect(() => { createEffect(() => {
if (!subscriptions || !props.topic) return if (!(subscriptions && props.topic)) return
const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id) const subscribed = subscriptions.topics?.some((topics) => topics.id === props.topic?.id)
setIsSubscribed(subscribed) setIsSubscribed(subscribed)
}) })

View File

@ -2,11 +2,11 @@ import type { Author } from '../../../graphql/schema/core.gen'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' import { For, Show, createMemo, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { setAuthorsSort, useAuthorsStore } from '../../../stores/zine/authors' import { useAuthorsStore } from '../../../stores/zine/authors'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { scrollHandler } from '../../../utils/scroll' import { scrollHandler } from '../../../utils/scroll'
import { authorLetterReduce, translateAuthor } from '../../../utils/translate' import { authorLetterReduce, translateAuthor } from '../../../utils/translate'
@ -33,7 +33,7 @@ export const AllAuthors = (props: Props) => {
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')
const ALPHABET = const ALPHABET =
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@'] lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@']
const { searchParams, changeSearchParams } = useRouter<AllAuthorsPageSearchParams>() const { searchParams } = useRouter<AllAuthorsPageSearchParams>()
const { sortedAuthors } = useAuthorsStore({ const { sortedAuthors } = useAuthorsStore({
authors: props.authors, authors: props.authors,
sortBy: searchParams().by || 'name', sortBy: searchParams().by || 'name',

View File

@ -3,8 +3,6 @@ import type { Topic } from '../../../graphql/schema/core.gen'
import { Meta } from '@solidjs/meta' import { Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics' import { setTopicsSort, useTopicsStore } from '../../../stores/zine/topics'

View File

@ -11,7 +11,7 @@ import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core' import { apiClient } from '../../../graphql/client/core'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { loadShouts, useArticlesStore } from '../../../stores/zine/articles' import { loadShouts, useArticlesStore } from '../../../stores/zine/articles'
import { loadAuthor, useAuthorsStore } from '../../../stores/zine/authors' import { loadAuthor } from '../../../stores/zine/authors'
import { getImageUrl } from '../../../utils/getImageUrl' import { getImageUrl } from '../../../utils/getImageUrl'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
@ -39,10 +39,9 @@ const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: Props) => { export const AuthorView = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { subscriptions, followers: myFollowers, loadSubscriptions } = useFollowing() const { followers: myFollowers } = useFollowing()
const { session } = useSession() const { session } = useSession()
const { sortedArticles } = useArticlesStore({ shouts: props.shouts }) const { sortedArticles } = useArticlesStore({ shouts: props.shouts })
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const { page: getPage, searchParams } = useRouter() const { page: getPage, searchParams } = useRouter()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
@ -87,7 +86,7 @@ export const AuthorView = (props: Props) => {
setFollowing([...(authors || []), ...(topics || [])]) setFollowing([...(authors || []), ...(topics || [])])
setFollowers(followersResult || []) setFollowers(followersResult || [])
console.info('[components.Author] data loaded') console.debug('[components.Author] following data loaded', subscriptionsResult)
} catch (error) { } catch (error) {
console.error('[components.Author] fetch error', error) console.error('[components.Author] fetch error', error)
} }

View File

@ -1,6 +1,6 @@
import { openPage } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal } from 'solid-js' import { For, Show, createEffect, createSignal, on } from 'solid-js'
import { useEditorContext } from '../../../context/editor' import { useEditorContext } from '../../../context/editor'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -9,22 +9,24 @@ import { Shout } from '../../../graphql/schema/core.gen'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { Draft } from '../../Draft' import { Draft } from '../../Draft'
import { Loading } from '../../_shared/Loading'
import styles from './DraftsView.module.scss' import styles from './DraftsView.module.scss'
export const DraftsView = () => { export const DraftsView = () => {
const { isAuthenticated, isSessionLoaded } = useSession() const { session } = useSession()
const [drafts, setDrafts] = createSignal<Shout[]>([]) const [drafts, setDrafts] = createSignal<Shout[]>([])
const loadDrafts = async () => { createEffect(
if (apiClient.private) { on(
const loadedDrafts = await apiClient.getDrafts() () => session(),
setDrafts(loadedDrafts.reverse() || []) async (s) => {
} if (s) {
} const loadedDrafts = await apiClient.getDrafts()
setDrafts(loadedDrafts.reverse() || [])
createEffect(() => { }
if (isSessionLoaded()) loadDrafts() },
}) ),
)
const { publishShoutById, deleteShout } = useEditorContext() const { publishShoutById, deleteShout } = useEditorContext()
@ -44,22 +46,20 @@ export const DraftsView = () => {
return ( return (
<div class={clsx(styles.DraftsView)}> <div class={clsx(styles.DraftsView)}>
<Show when={isSessionLoaded()}> <Show when={session()?.user?.id} fallback={<Loading />}>
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<div class="col-md-19 col-lg-18 col-xl-16 offset-md-5"> <div class="col-md-19 col-lg-18 col-xl-16 offset-md-5">
<Show when={isAuthenticated()} fallback="Давайте авторизуемся"> <For each={drafts()}>
<For each={drafts()}> {(draft) => (
{(draft) => ( <Draft
<Draft class={styles.draft}
class={styles.draft} shout={draft}
shout={draft} onDelete={handleDraftDelete}
onDelete={handleDraftDelete} onPublish={handleDraftPublish}
onPublish={handleDraftPublish} />
/> )}
)} </For>
</For>
</Show>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,7 @@ import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js' import { Accessor, Show, createMemo, createSignal, lazy, onCleanup, onMount } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { throttle } from 'throttle-debounce'
import { ShoutForm, useEditorContext } from '../../../context/editor' import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -41,7 +42,9 @@ export const EMPTY_TOPIC: Topic = {
slug: '', slug: '',
} }
const THROTTLING_INTERVAL = 2000
const AUTO_SAVE_INTERVAL = 5000 const AUTO_SAVE_INTERVAL = 5000
const AUTO_SAVE_DELAY = 5000
const handleScrollTopButtonClick = (e) => { const handleScrollTopButtonClick = (e) => {
e.preventDefault() e.preventDefault()
window.scrollTo({ window.scrollTo({
@ -66,8 +69,9 @@ export const EditView = (props: Props) => {
const shoutTopics = props.shout.topics || [] const shoutTopics = props.shout.topics || []
// TODO: проверить сохранение черновика в local storage (не работает) // TODO: проверить сохранение черновика в local storage (не работает)
const draft = getDraftFromLocalStorage(props.shout.id) const draft = props.shout || getDraftFromLocalStorage(props.shout.id)
if (draft) { if (draft) {
// console.debug('draft: ', draft)
setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id }) setForm(Object.keys(draft).length !== 0 ? draft : { shoutId: props.shout.id })
} else { } else {
setForm({ setForm({
@ -180,42 +184,43 @@ export const EditView = (props: Props) => {
let autoSaveTimeOutId: number | string | NodeJS.Timeout let autoSaveTimeOutId: number | string | NodeJS.Timeout
//TODO: add throttle const autoSave = async () => {
const autoSaveRecursive = () => { const hasChanges = !deepEqual(form, prevForm)
autoSaveTimeOutId = setTimeout(async () => { const hasTopic = Boolean(form.mainTopic)
const hasChanges = !deepEqual(form, prevForm) if (hasChanges && hasTopic) {
if (hasChanges) { setSaving(true)
setSaving(true) if (props.shout?.published_at) {
if (props.shout?.published_at) { saveDraftToLocalStorage(form)
saveDraftToLocalStorage(form) } else {
} else { await saveDraft(form)
await saveDraft(form)
}
setPrevForm(clone(form))
setTimeout(() => {
setSaving(false)
}, 2000)
} }
setPrevForm(clone(form))
setTimeout(() => {
setSaving(false)
}, AUTO_SAVE_DELAY)
}
}
// Throttle the autoSave function
const throttledAutoSave = throttle(THROTTLING_INTERVAL, autoSave)
const autoSaveRecursive = () => {
autoSaveTimeOutId = setTimeout(() => {
throttledAutoSave()
autoSaveRecursive() autoSaveRecursive()
}, AUTO_SAVE_INTERVAL) }, AUTO_SAVE_INTERVAL)
} }
const stopAutoSave = () => {
clearTimeout(autoSaveTimeOutId)
}
onMount(() => { onMount(() => {
autoSaveRecursive() autoSaveRecursive()
}) onCleanup(() => clearTimeout(autoSaveTimeOutId))
onCleanup(() => {
stopAutoSave()
}) })
const showSubtitleInput = () => { const showSubtitleInput = () => {
setIsSubtitleVisible(true) setIsSubtitleVisible(true)
subtitleInput.current.focus() subtitleInput.current.focus()
} }
const showLeadInput = () => { const showLeadInput = () => {
setIsLeadVisible(true) setIsLeadVisible(true)
} }

View File

@ -49,7 +49,7 @@ type VisibilityItem = {
} }
type FeedSearchParams = { type FeedSearchParams = {
by: 'publish_date' | 'likes' | 'comments' by: 'publish_date' | 'likes' | 'last_comment'
period: FeedPeriod period: FeedPeriod
visibility: VisibilityMode visibility: VisibilityMode
} }
@ -258,10 +258,10 @@ export const FeedView = (props: Props) => {
</li> </li>
<li <li
class={clsx({ class={clsx({
'view-switcher__item--selected': searchParams().by === 'comments', 'view-switcher__item--selected': searchParams().by === 'last_comment',
})} })}
> >
<span class="link" onClick={() => changeSearchParams({ by: 'comments' })}> <span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
{t('Most commented')} {t('Most commented')}
</span> </span>
</li> </li>

View File

@ -1,10 +1,8 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal, onMount } from 'solid-js' import { For, Show, createEffect, createSignal } from 'solid-js'
import { useFollowing } from '../../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core'
import { Author, Topic } from '../../../graphql/schema/core.gen' import { Author, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { dummyFilter } from '../../../utils/dummyFilter' import { dummyFilter } from '../../../utils/dummyFilter'
@ -21,7 +19,6 @@ import stylesSettings from '../../../styles/FeedSettings.module.scss'
export const ProfileSubscriptions = () => { export const ProfileSubscriptions = () => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { author, session } = useSession()
const { subscriptions } = useFollowing() const { subscriptions } = useFollowing()
const [following, setFollowing] = createSignal<Array<Author | Topic>>([]) const [following, setFollowing] = createSignal<Array<Author | Topic>>([])
const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([]) const [filtered, setFiltered] = createSignal<Array<Author | Topic>>([])

View File

@ -1,6 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo } from 'solid-js' import { Show, createMemo } from 'solid-js'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Button } from '../Button' import { Button } from '../Button'
import stylesButton from '../Button/Button.module.scss' import stylesButton from '../Button/Button.module.scss'

View File

@ -1 +1 @@
export * from './Icon' export { Icon } from './Icon'

View File

@ -1 +1 @@
export * from './Popup' export { Popup } from './Popup'

View File

@ -25,7 +25,7 @@ const setupIndexedDB = async () => {
}) })
} }
const getTopicsFromIndexedDB = async (db) => { const getTopicsFromIndexedDB = (db) => {
const tx = db.transaction(STORE_NAME, 'readonly') const tx = db.transaction(STORE_NAME, 'readonly')
const store = tx.objectStore(STORE_NAME) const store = tx.objectStore(STORE_NAME)
return store.getAll() return store.getAll()
@ -44,7 +44,7 @@ export const TopicsProvider = (props: { children: JSX.Element }) => {
onMount(async () => { onMount(async () => {
const db = await setupIndexedDB() const db = await setupIndexedDB()
let topics = await getTopicsFromIndexedDB(db) let topics = getTopicsFromIndexedDB(db)
if (topics.length === 0) { if (topics.length === 0) {
topics = await apiClient.getAllTopics() topics = await apiClient.getAllTopics()

View File

@ -1,6 +1,6 @@
import type { PageProps } from './types' import type { PageProps } from './types'
import { createEffect, createSignal, onMount } from 'solid-js' import { createSignal, onMount } from 'solid-js'
import { AllAuthors } from '../components/Views/AllAuthors/' import { AllAuthors } from '../components/Views/AllAuthors/'
import { PAGE_SIZE } from '../components/Views/AllTopics/AllTopics' import { PAGE_SIZE } from '../components/Views/AllTopics/AllTopics'

View File

@ -1,12 +1,12 @@
import { Show, Suspense, createMemo, createSignal, lazy, onMount } from 'solid-js' import { Show, createEffect, createMemo, createSignal, lazy, on, onMount } from 'solid-js'
import { AuthGuard } from '../components/AuthGuard' import { AuthGuard } from '../components/AuthGuard'
import { Loading } from '../components/_shared/Loading' import { Loading } from '../components/_shared/Loading'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
import { useLocalize } from '../context/localize' import { useLocalize } from '../context/localize'
import { useSession } from '../context/session'
import { apiClient } from '../graphql/client/core' import { apiClient } from '../graphql/client/core'
import { Shout } from '../graphql/schema/core.gen' import { Shout } from '../graphql/schema/core.gen'
import { useRouter } from '../stores/router'
import { router } from '../stores/router' import { router } from '../stores/router'
import { redirectPage } from '@nanostores/router' import { redirectPage } from '@nanostores/router'
@ -15,67 +15,70 @@ import { LayoutType } from './types'
const EditView = lazy(() => import('../components/Views/EditView/EditView')) const EditView = lazy(() => import('../components/Views/EditView/EditView'))
export const EditPage = () => { const getContentTypeTitle = (layout: LayoutType) => {
const { page } = useRouter() switch (layout) {
const snackbar = useSnackbar() case 'audio':
const { t } = useLocalize() return 'Publish Album'
case 'image':
return 'Create gallery'
case 'video':
return 'Create video'
case 'literature':
return 'New literary work'
default:
return 'Write an article'
}
}
const [shout, setShout] = createSignal<Shout>(null) export const EditPage = () => {
const loadMyShout = async (shout_id: number) => { const { t } = useLocalize()
if (shout_id) { const { session } = useSession()
const { shout: loadedShout, error } = await apiClient.getMyShout(shout_id) const snackbar = useSnackbar()
console.log(loadedShout)
if (error) { const fail = async (error: string) => {
await snackbar?.showSnackbar({ type: 'error', body: t('This content is not published yet') }) console.error(error)
redirectPage(router, 'drafts') await snackbar?.showSnackbar({ type: 'error', body: t(error) })
} else { redirectPage(router, 'drafts')
setShout(loadedShout)
}
}
} }
onMount(async () => { const [shoutId, setShoutId] = createSignal<number>(0)
const shout_id = window.location.pathname.split('/').pop() const [shout, setShout] = createSignal<Shout>()
if (shout_id) {
try { onMount(() => {
await loadMyShout(parseInt(shout_id, 10)) const shoutId = window.location.pathname.split('/').pop()
} catch (e) { const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
console.error(e) if (shoutIdFromUrl) setShoutId(shoutIdFromUrl)
}
}
}) })
createEffect(
on(
[session, shout, shoutId],
async ([ses, sh, shid]) => {
if (ses?.user && !sh && shid) {
const { shout: loadedShout, error } = await apiClient.getMyShout(shid)
if (error) {
fail(error)
} else {
setShout(loadedShout)
}
}
},
{ defer: true },
),
)
const title = createMemo(() => { const title = createMemo(() => {
if (!shout()) { if (!shout()) {
return t('Create post') return t('Create post')
} }
return t(getContentTypeTitle(shout()?.layout as LayoutType))
switch (shout().layout as LayoutType) {
case 'audio': {
return t('Publish Album')
}
case 'image': {
return t('Create gallery')
}
case 'video': {
return t('Create video')
}
case 'literature': {
return t('New literary work')
}
default: {
return t('Write an article')
}
}
}) })
return ( return (
<PageLayout title={title()}> <PageLayout title={title()}>
<AuthGuard> <AuthGuard>
<Show when={shout()}> <Show when={shout()} fallback={<Loading />}>
<Suspense fallback={<Loading />}> <EditView shout={shout() as Shout} />
<EditView shout={shout()} />
</Suspense>
</Show> </Show>
</AuthGuard> </AuthGuard>
</PageLayout> </PageLayout>

View File

@ -2,6 +2,7 @@ import { RANDOM_TOPICS_COUNT } from '../components/Views/Home'
import { Topic } from '../graphql/schema/core.gen' import { Topic } from '../graphql/schema/core.gen'
export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => { export const getRandomTopicsFromArray = (topics: Topic[], count: number = RANDOM_TOPICS_COUNT): Topic[] => {
if (!Array.isArray(topics)) return []
const shuffledTopics = [...topics].sort(() => 0.5 - Math.random()) const shuffledTopics = [...topics].sort(() => 0.5 - Math.random())
return shuffledTopics.slice(0, count) return shuffledTopics.slice(0, count)
} }

View File

@ -66,7 +66,7 @@ export default defineConfig(({ mode, command }) => {
}, },
build: { build: {
rollupOptions: { rollupOptions: {
external: [], external: ['buffer'],
}, },
chunkSizeWarningLimit: 1024, chunkSizeWarningLimit: 1024,
target: 'esnext', target: 'esnext',