Merge branch 'hotfix/editor-permission' into feature/rating
This commit is contained in:
commit
a89a9bb1f4
49
biome.json
49
biome.json
|
@ -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",
|
||||||
|
|
3230
package-lock.json
generated
3230
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
|
@ -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",
|
||||||
|
@ -35,7 +35,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,8 +129,9 @@
|
||||||
"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-node-polyfills": "0.21.0",
|
||||||
"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",
|
||||||
"y-prosemirror": "1.2.2",
|
"y-prosemirror": "1.2.2",
|
||||||
|
@ -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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,15 +40,11 @@ import { InboxPage } from '../pages/inbox.page'
|
||||||
import { HomePage } from '../pages/index.page'
|
import { HomePage } from '../pages/index.page'
|
||||||
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
|
||||||
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
|
||||||
//TODO: ProfileSubscriptionsPage - garbage code?
|
|
||||||
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
|
||||||
import { SearchPage } from '../pages/search.page'
|
import { SearchPage } from '../pages/search.page'
|
||||||
import { TopicPage } from '../pages/topic.page'
|
import { TopicPage } from '../pages/topic.page'
|
||||||
import { ROUTES, useRouter } from '../stores/router'
|
import { ROUTES, useRouter } from '../stores/router'
|
||||||
import { MODALS, hideModal, showModal } from '../stores/ui'
|
import { MODALS, showModal } from '../stores/ui'
|
||||||
|
|
||||||
// TODO: lazy load
|
|
||||||
// const SomePage = lazy(() => import('./Pages/SomePage'))
|
|
||||||
|
|
||||||
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
|
||||||
author: AuthorPage,
|
author: AuthorPage,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { openPage } from '@nanostores/router'
|
import { openPage } from '@nanostores/router'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
|
import { Match, Show, Switch, createEffect, createMemo, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useFollowing } from '../../../context/following'
|
import { useFollowing } from '../../../context/following'
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -127,6 +127,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>
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { Buffer } from 'buffer'
|
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show } from 'solid-js'
|
import { Show } from 'solid-js'
|
||||||
|
|
||||||
|
@ -9,6 +7,7 @@ import { composeMediaItems } from '../../../utils/composeMediaItems'
|
||||||
import { AudioPlayer } from '../../Article/AudioPlayer'
|
import { AudioPlayer } from '../../Article/AudioPlayer'
|
||||||
import { DropArea } from '../../_shared/DropArea'
|
import { DropArea } from '../../_shared/DropArea'
|
||||||
|
|
||||||
|
// import { Buffer } from 'node:buffer'
|
||||||
import styles from './AudioUploader.module.scss'
|
import styles from './AudioUploader.module.scss'
|
||||||
|
|
||||||
window.Buffer = Buffer
|
window.Buffer = Buffer
|
||||||
|
|
|
@ -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]) {
|
||||||
|
|
|
@ -29,9 +29,11 @@ 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)
|
||||||
|
if (attribute) {
|
||||||
result[attribute.name] = attribute.value
|
result[attribute.name] = attribute.value
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'] })
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { JSX, Show, createEffect, createSignal } from 'solid-js'
|
import { JSX, Show, createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../../context/localize'
|
import { useLocalize } from '../../../context/localize'
|
||||||
import { useSession } from '../../../context/session'
|
import { useSession } from '../../../context/session'
|
||||||
|
@ -31,8 +31,8 @@ export const LoginForm = () => {
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
|
||||||
|
// FIXME: use signal or remove
|
||||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
const [_isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
const authFormRef: { current: HTMLFormElement } = { current: null }
|
const authFormRef: { current: HTMLFormElement } = { current: null }
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
const { signIn } = useSession()
|
const { signIn } = useSession()
|
||||||
|
|
|
@ -32,7 +32,8 @@ export const RegisterForm = () => {
|
||||||
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
const { signUp, isRegistered, resendVerifyEmail } = useSession()
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
// FIXME: use submit error data or remove signal
|
||||||
|
const [_submitError, setSubmitError] = createSignal('')
|
||||||
const [fullName, setFullName] = createSignal('')
|
const [fullName, setFullName] = createSignal('')
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
const [isSubmitting, setIsSubmitting] = createSignal(false)
|
||||||
|
@ -116,7 +117,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 +130,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 +144,8 @@ export const RegisterForm = () => {
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
case 'registered':
|
}
|
||||||
|
case 'registered': {
|
||||||
setValidationErrors((prev) => ({
|
setValidationErrors((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
email: (
|
email: (
|
||||||
|
@ -156,11 +159,13 @@ export const RegisterForm = () => {
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
break
|
break
|
||||||
default:
|
}
|
||||||
|
default: {
|
||||||
console.info('[RegisterForm] email is not registered')
|
console.info('[RegisterForm] email is not registered')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleEmailBlur = async () => {
|
const handleEmailBlur = async () => {
|
||||||
if (validateEmail(email())) {
|
if (validateEmail(email())) {
|
||||||
|
|
|
@ -59,7 +59,8 @@ export const HeaderAuth = (props: Props) => {
|
||||||
toggleEditorPanel()
|
toggleEditorPanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSaveButtonClick = () => {
|
// FIXME: remove the code if not needed here
|
||||||
|
const _handleSaveButtonClick = () => {
|
||||||
saveShout(form)
|
saveShout(form)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const NotificationGroup = (props: NotificationGroupProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<For each={props.notifications}>
|
<For each={props.notifications}>
|
||||||
{(n: Group, index) => (
|
{(n: Group, _index) => (
|
||||||
<>
|
<>
|
||||||
{t(threadCaption(n.thread), { commentsCount: n.reactions.length })}{' '}
|
{t(threadCaption(n.thread), { commentsCount: n.reactions.length })}{' '}
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { Show, createEffect, createSignal, on } from 'solid-js'
|
import { 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'
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
() => session(),
|
||||||
|
async (s) => {
|
||||||
|
if (s) {
|
||||||
const loadedDrafts = await apiClient.getDrafts()
|
const loadedDrafts = await apiClient.getDrafts()
|
||||||
setDrafts(loadedDrafts.reverse() || [])
|
setDrafts(loadedDrafts.reverse() || [])
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
),
|
||||||
createEffect(() => {
|
)
|
||||||
if (isSessionLoaded()) loadDrafts()
|
|
||||||
})
|
|
||||||
|
|
||||||
const { publishShoutById, deleteShout } = useEditorContext()
|
const { publishShoutById, deleteShout } = useEditorContext()
|
||||||
|
|
||||||
|
@ -44,11 +46,10 @@ 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
|
||||||
|
@ -59,7 +60,6 @@ export const DraftsView = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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,11 +184,10 @@ 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 = () => {
|
|
||||||
autoSaveTimeOutId = setTimeout(async () => {
|
|
||||||
const hasChanges = !deepEqual(form, prevForm)
|
const hasChanges = !deepEqual(form, prevForm)
|
||||||
if (hasChanges) {
|
const hasTopic = Boolean(form.mainTopic)
|
||||||
|
if (hasChanges && hasTopic) {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
if (props.shout?.published_at) {
|
if (props.shout?.published_at) {
|
||||||
saveDraftToLocalStorage(form)
|
saveDraftToLocalStorage(form)
|
||||||
|
@ -194,28 +197,30 @@ export const EditView = (props: Props) => {
|
||||||
setPrevForm(clone(form))
|
setPrevForm(clone(form))
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSaving(false)
|
setSaving(false)
|
||||||
}, 2000)
|
}, 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { For, Show, batch, createMemo, createSignal, onMount } from 'solid-js'
|
import { For, Show, createMemo, createSignal, onMount } from 'solid-js'
|
||||||
|
|
||||||
import { useLocalize } from '../../context/localize'
|
import { useLocalize } from '../../context/localize'
|
||||||
import { apiClient } from '../../graphql/client/core'
|
|
||||||
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
import { Shout, Topic } from '../../graphql/schema/core.gen'
|
||||||
import { router } from '../../stores/router'
|
import { router } from '../../stores/router'
|
||||||
import {
|
import {
|
||||||
|
@ -52,8 +51,8 @@ export const HomeView = (props: Props) => {
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
const { t } = useLocalize()
|
const { t } = useLocalize()
|
||||||
|
|
||||||
const [randomTopic, setRandomTopic] = createSignal<Topic>(null)
|
const [randomTopic, _setRandomTopic] = createSignal<Topic>(null)
|
||||||
const [randomTopicArticles, setRandomTopicArticles] = createSignal<Shout[]>([])
|
const [randomTopicArticles, _setRandomTopicArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
loadTopArticles()
|
loadTopArticles()
|
||||||
|
|
|
@ -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>>([])
|
||||||
|
|
|
@ -127,10 +127,10 @@ export const PublishSettings = (props: Props) => {
|
||||||
}
|
}
|
||||||
const handlePublishSubmit = () => {
|
const handlePublishSubmit = () => {
|
||||||
const shoutData = { ...props.form, ...settingsForm }
|
const shoutData = { ...props.form, ...settingsForm }
|
||||||
if (!shoutData?.mainTopic) {
|
if (shoutData?.mainTopic) {
|
||||||
showSnackbar({ body: t('Please, set the main topic first') })
|
|
||||||
} else {
|
|
||||||
publishShout(shoutData)
|
publishShout(shoutData)
|
||||||
|
} else {
|
||||||
|
showSnackbar({ body: t('Please, set the main topic first') })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleSaveDraft = () => {
|
const handleSaveDraft = () => {
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './Icon'
|
export { Icon } from './Icon'
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { useAuthorsStore } from '../../../stores/zine/authors'
|
||||||
import { AuthorBadge } from '../../Author/AuthorBadge'
|
import { AuthorBadge } from '../../Author/AuthorBadge'
|
||||||
import { Button } from '../Button'
|
import { Button } from '../Button'
|
||||||
import { DropdownSelect } from '../DropdownSelect'
|
import { DropdownSelect } from '../DropdownSelect'
|
||||||
import { Loading } from '../Loading'
|
|
||||||
|
|
||||||
import { InlineLoader } from '../../InlineLoader'
|
import { InlineLoader } from '../../InlineLoader'
|
||||||
import styles from './InviteMembers.module.scss'
|
import styles from './InviteMembers.module.scss'
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export * from './Popup'
|
export { Popup, type PopupProps } from './Popup'
|
||||||
|
|
|
@ -57,11 +57,11 @@ export const ImageSwiper = (props: Props) => {
|
||||||
const { register } = await import('swiper/element/bundle')
|
const { register } = await import('swiper/element/bundle')
|
||||||
register()
|
register()
|
||||||
SwiperCore.use([Pagination, Navigation, Manipulation, HashNavigation])
|
SwiperCore.use([Pagination, Navigation, Manipulation, HashNavigation])
|
||||||
while (!mainSwipeRef.current || !mainSwipeRef.current.swiper) {
|
while (!mainSwipeRef.current?.swiper) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10)) // wait 10 ms
|
await new Promise((resolve) => setTimeout(resolve, 10)) // wait 10 ms
|
||||||
}
|
}
|
||||||
mainSwipeRef.current.swiper.on('slideChange', handleSlideChange)
|
mainSwipeRef.current.swiper.on('slideChange', handleSlideChange)
|
||||||
const initialSlide = parseInt(searchParams().slide) - 1
|
const initialSlide = Number.parseInt(searchParams().slide) - 1
|
||||||
if (initialSlide && !Number.isNaN(initialSlide) && initialSlide < props.images.length) {
|
if (initialSlide && !Number.isNaN(initialSlide) && initialSlide < props.images.length) {
|
||||||
mainSwipeRef.current.swiper.slideTo(initialSlide, 0)
|
mainSwipeRef.current.swiper.slideTo(initialSlide, 0)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Accessor, JSX, createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js'
|
import { Accessor, JSX, createContext, createEffect, createSignal, useContext } from 'solid-js'
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
|
@ -16,7 +16,7 @@ type SubscribeAction = { slug: string; type: 'subscribe' | 'unsubscribe' }
|
||||||
|
|
||||||
interface FollowingContextType {
|
interface FollowingContextType {
|
||||||
loading: Accessor<boolean>
|
loading: Accessor<boolean>
|
||||||
followers: Accessor<Array<Author>>
|
followers: Accessor<Author[]>
|
||||||
subscriptions: AuthorFollowsResult
|
subscriptions: AuthorFollowsResult
|
||||||
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
setSubscriptions: (subscriptions: AuthorFollowsResult) => void
|
||||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||||
|
@ -41,7 +41,7 @@ const EMPTY_SUBSCRIPTIONS: AuthorFollowsResult = {
|
||||||
|
|
||||||
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
const [followers, setFollowers] = createSignal<Array<Author>>([])
|
const [followers, setFollowers] = createSignal<Author[]>([])
|
||||||
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
const [subscriptions, setSubscriptions] = createStore<AuthorFollowsResult>(EMPTY_SUBSCRIPTIONS)
|
||||||
const { author, session } = useSession()
|
const { author, session } = useSession()
|
||||||
|
|
||||||
|
|
|
@ -72,6 +72,7 @@ export type SessionContextType = {
|
||||||
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
resendVerifyEmail: (params: ResendVerifyEmailInput) => Promise<GenericResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/suspicious/noEmptyBlockStatements: <explanation>
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
|
|
||||||
const SessionContext = createContext<SessionContextType>()
|
const SessionContext = createContext<SessionContextType>()
|
||||||
|
@ -270,12 +271,9 @@ export const SessionProvider = (props: {
|
||||||
|
|
||||||
// callback state updater
|
// callback state updater
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on([() => props.onStateChangeCallback, session], ([_, ses]) => {
|
||||||
() => props.onStateChangeCallback,
|
ses?.user?.id && props.onStateChangeCallback(ses)
|
||||||
() => {
|
}),
|
||||||
props.onStateChangeCallback(session())
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
const [authCallback, setAuthCallback] = createSignal<() => void>(noop)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { createGraphQLClient } from '../createGraphQLClient'
|
|
||||||
import markSeenMutation from '../mutation/notifier/mark-seen'
|
import markSeenMutation from '../mutation/notifier/mark-seen'
|
||||||
import markSeenAfterMutation from '../mutation/notifier/mark-seen-after'
|
import markSeenAfterMutation from '../mutation/notifier/mark-seen-after'
|
||||||
import markThreadSeenMutation from '../mutation/notifier/mark-seen-thread'
|
import markThreadSeenMutation from '../mutation/notifier/mark-seen-thread'
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Show, Suspense, createMemo, createSignal, lazy, onMount } from 'solid-js'
|
import { Show, Suspense, 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,68 +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)
|
||||||
|
await snackbar?.showSnackbar({ type: 'error', body: t(error) })
|
||||||
redirectPage(router, 'drafts')
|
redirectPage(router, 'drafts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const [shoutId, setShoutId] = createSignal<number>(0)
|
||||||
|
const [shout, setShout] = createSignal<Shout>()
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const shoutId = window.location.pathname.split('/').pop()
|
||||||
|
const shoutIdFromUrl = Number.parseInt(shoutId ?? '0', 10)
|
||||||
|
console.debug(`editing shout ${shoutIdFromUrl}`)
|
||||||
|
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 {
|
} else {
|
||||||
setShout(loadedShout)
|
setShout(loadedShout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
|
)
|
||||||
onMount(async () => {
|
|
||||||
const shout_id = window.location.pathname.split('/').pop()
|
|
||||||
if (shout_id) {
|
|
||||||
try {
|
|
||||||
await loadMyShout(parseInt(shout_id, 10))
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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()}>
|
|
||||||
<Suspense fallback={<Loading />}>
|
<Suspense fallback={<Loading />}>
|
||||||
<EditView shout={shout()} />
|
<Show when={shout()} fallback={<Loading />}>
|
||||||
</Suspense>
|
<EditView shout={shout() as Shout} />
|
||||||
</Show>
|
</Show>
|
||||||
|
</Suspense>
|
||||||
</AuthGuard>
|
</AuthGuard>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -69,7 +69,8 @@ const checkOpenOnClient = (link: HTMLAnchorElement, event) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToHash = (hash: string) => {
|
// TODO: use scrollToHash or remove
|
||||||
|
const _scrollToHash = (hash: string) => {
|
||||||
let selector = hash
|
let selector = hash
|
||||||
|
|
||||||
if (/^#\d+/.test(selector)) {
|
if (/^#\d+/.test(selector)) {
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { createLazyMemo } from '@solid-primitives/memo'
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
import { createEffect, createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
import { apiClient } from '../../graphql/client/core'
|
import { apiClient } from '../../graphql/client/core'
|
||||||
import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
||||||
|
|
||||||
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
||||||
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
||||||
|
// FIXME: use signal or remove
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
const [_sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
||||||
|
|
||||||
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import ssrPlugin from 'vike/plugin'
|
import ssrPlugin from 'vike/plugin'
|
||||||
import { defineConfig, splitVendorChunkPlugin } from 'vite'
|
import { defineConfig, splitVendorChunkPlugin } from 'vite'
|
||||||
import mkcert from 'vite-plugin-mkcert'
|
import mkcert from 'vite-plugin-mkcert'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills';
|
||||||
import sassDts from 'vite-plugin-sass-dts'
|
import sassDts from 'vite-plugin-sass-dts'
|
||||||
import solidPlugin from 'vite-plugin-solid'
|
import solidPlugin from 'vite-plugin-solid'
|
||||||
|
|
||||||
|
@ -40,6 +41,19 @@ export default defineConfig(({ mode, command }) => {
|
||||||
ssrPlugin({ includeAssetsImportedByServer: true }),
|
ssrPlugin({ includeAssetsImportedByServer: true }),
|
||||||
sassDts(),
|
sassDts(),
|
||||||
cssModuleHMR(),
|
cssModuleHMR(),
|
||||||
|
nodePolyfills({
|
||||||
|
include: ['path', 'stream', 'util'],
|
||||||
|
exclude: ['http'],
|
||||||
|
globals: {
|
||||||
|
Buffer: true,
|
||||||
|
//global: true,
|
||||||
|
//process: true,
|
||||||
|
},
|
||||||
|
overrides: {
|
||||||
|
fs: 'memfs',
|
||||||
|
},
|
||||||
|
protocolImports: true,
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
if (command === 'serve') {
|
if (command === 'serve') {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user