merged
This commit is contained in:
commit
8b230d2a03
13
.eslintrc.js
13
.eslintrc.js
|
@ -34,10 +34,8 @@ module.exports = {
|
||||||
varsIgnorePattern: '^log$'
|
varsIgnorePattern: '^log$'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
// TODO: Remove any usage and enable
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
// TODO: Fix errors and enable this rule
|
|
||||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
||||||
|
|
||||||
// solid-js fix
|
// solid-js fix
|
||||||
'import/no-unresolved': [2, { ignore: ['solid-js/'] }]
|
'import/no-unresolved': [2, { ignore: ['solid-js/'] }]
|
||||||
|
@ -58,8 +56,8 @@ module.exports = {
|
||||||
// FIXME
|
// FIXME
|
||||||
'solid/reactivity': 'off',
|
'solid/reactivity': 'off',
|
||||||
|
|
||||||
// TODO: Should be enabled
|
// Should be enabled
|
||||||
'promise/catch-or-return': 'off',
|
// 'promise/catch-or-return': 'off',
|
||||||
|
|
||||||
'solid/no-innerhtml': 'off',
|
'solid/no-innerhtml': 'off',
|
||||||
|
|
||||||
|
@ -77,7 +75,8 @@ module.exports = {
|
||||||
|
|
||||||
eqeqeq: 'error',
|
eqeqeq: 'error',
|
||||||
'no-param-reassign': 'error',
|
'no-param-reassign': 'error',
|
||||||
'no-nested-ternary': 'error'
|
'no-nested-ternary': 'error',
|
||||||
|
'no-shadow': 'error'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"@nanostores/persistent": "^0.7.0",
|
"@nanostores/persistent": "^0.7.0",
|
||||||
"@nanostores/router": "^0.7.0",
|
"@nanostores/router": "^0.7.0",
|
||||||
"@nanostores/solid": "^0.3.0",
|
"@nanostores/solid": "^0.3.0",
|
||||||
|
"@solid-primitives/memo": "^1.0.2",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"google-translate-api-x": "^10.4.1",
|
"google-translate-api-x": "^10.4.1",
|
||||||
"loglevel": "^1.8.0",
|
"loglevel": "^1.8.0",
|
||||||
|
|
|
@ -7,8 +7,7 @@ import { createEffect, createMemo, createSignal, For, onMount, Show } from 'soli
|
||||||
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { useStore } from '@nanostores/solid'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { session } from '../../stores/auth'
|
|
||||||
import { incrementView } from '../../stores/zine/articles'
|
import { incrementView } from '../../stores/zine/articles'
|
||||||
import { renderMarkdown } from '@astrojs/markdown-remark'
|
import { renderMarkdown } from '@astrojs/markdown-remark'
|
||||||
import { markdownOptions } from '../../../mdx.config'
|
import { markdownOptions } from '../../../mdx.config'
|
||||||
|
@ -41,7 +40,7 @@ const formatDate = (date: Date) => {
|
||||||
export const FullArticle = (props: ArticleProps) => {
|
export const FullArticle = (props: ArticleProps) => {
|
||||||
const [body, setBody] = createSignal(props.article.body?.startsWith('<') ? props.article.body : '')
|
const [body, setBody] = createSignal(props.article.body?.startsWith('<') ? props.article.body : '')
|
||||||
|
|
||||||
const auth = useStore(session)
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (body() || !props.article.body) {
|
if (body() || !props.article.body) {
|
||||||
|
@ -51,7 +50,9 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
if (props.article.body.startsWith('<')) {
|
if (props.article.body.startsWith('<')) {
|
||||||
setBody(props.article.body)
|
setBody(props.article.body)
|
||||||
} else {
|
} else {
|
||||||
renderMarkdown(props.article.body, markdownOptions).then(({ code }) => setBody(code))
|
renderMarkdown(props.article.body, markdownOptions)
|
||||||
|
.then(({ code }) => setBody(code))
|
||||||
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -180,12 +181,12 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
<ArticleComment
|
<ArticleComment
|
||||||
comment={reaction}
|
comment={reaction}
|
||||||
level={getCommentLevel(reaction)}
|
level={getCommentLevel(reaction)}
|
||||||
canEdit={reaction.createdBy?.slug === auth()?.user?.slug}
|
canEdit={reaction.createdBy?.slug === session()?.user?.slug}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!auth()?.user?.slug}>
|
<Show when={!session()?.user?.slug}>
|
||||||
<div class="comment-warning" id="comments">
|
<div class="comment-warning" id="comments">
|
||||||
{t('To leave a comment you please')}
|
{t('To leave a comment you please')}
|
||||||
<a
|
<a
|
||||||
|
@ -199,7 +200,7 @@ export const FullArticle = (props: ArticleProps) => {
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={auth()?.user?.slug}>
|
<Show when={session()?.user?.slug}>
|
||||||
<textarea class="write-comment" rows="1" placeholder={t('Write comment')} />
|
<textarea class="write-comment" rows="1" placeholder={t('Write comment')} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,10 +6,9 @@ import './Card.scss'
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo } from 'solid-js'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { session } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
|
|
||||||
interface AuthorCardProps {
|
interface AuthorCardProps {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
@ -21,14 +20,15 @@ interface AuthorCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorCard = (props: AuthorCardProps) => {
|
export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
const auth = useStore(session)
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
const subscribed = createMemo(
|
const subscribed = createMemo(
|
||||||
() =>
|
() =>
|
||||||
!!auth()
|
!!session()
|
||||||
?.info?.authors?.filter((u) => u === props.author.slug)
|
?.info?.authors?.filter((u) => u === props.author.slug)
|
||||||
.pop()
|
.pop()
|
||||||
)
|
)
|
||||||
const canFollow = createMemo(() => !props.hideFollow && auth()?.user?.slug !== props.author.slug)
|
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
|
||||||
const bio = () => props.author.bio || t('Our regular contributor')
|
const bio = () => props.author.bio || t('Our regular contributor')
|
||||||
const name = () => {
|
const name = () => {
|
||||||
return props.author.name === 'Дискурс' && locale() !== 'ru'
|
return props.author.name === 'Дискурс' && locale() !== 'ru'
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
interface BesideProps {
|
interface BesideProps {
|
||||||
title: string
|
title?: string
|
||||||
values: any[]
|
values: any[]
|
||||||
beside: Shout
|
beside: Shout
|
||||||
wrapper: 'topic' | 'author' | 'article' | 'top-article'
|
wrapper: 'topic' | 'author' | 'article' | 'top-article'
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { For } from 'solid-js'
|
import { For } from 'solid-js'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { session } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
|
@ -15,13 +14,13 @@ type FeedSidebarProps = {
|
||||||
|
|
||||||
export const FeedSidebar = (props: FeedSidebarProps) => {
|
export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
const { getSeen: seen } = useSeenStore()
|
const { getSeen: seen } = useSeenStore()
|
||||||
const auth = useStore(session)
|
const { session } = useAuthStore()
|
||||||
const { getSortedAuthors: authors } = useAuthorsStore({ authors: props.authors })
|
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
||||||
const { getArticlesByTopic } = useArticlesStore()
|
const { articlesByTopic } = useArticlesStore()
|
||||||
const { getTopicEntities } = useTopicsStore()
|
const { topicEntities } = useTopicsStore()
|
||||||
|
|
||||||
const checkTopicIsSeen = (topicSlug: string) => {
|
const checkTopicIsSeen = (topicSlug: string) => {
|
||||||
return getArticlesByTopic()[topicSlug].every((article) => Boolean(seen()[article.slug]))
|
return articlesByTopic()[topicSlug].every((article) => Boolean(seen()[article.slug]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkAuthorIsSeen = (authorSlug: string) => {
|
const checkAuthorIsSeen = (authorSlug: string) => {
|
||||||
|
@ -64,22 +63,22 @@ export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<For each={auth()?.info?.authors}>
|
<For each={session()?.info?.authors}>
|
||||||
{(authorSlug) => (
|
{(authorSlug) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/author/${authorSlug}`} classList={{ unread: checkAuthorIsSeen(authorSlug) }}>
|
<a href={`/author/${authorSlug}`} classList={{ unread: checkAuthorIsSeen(authorSlug) }}>
|
||||||
<small>@{authorSlug}</small>
|
<small>@{authorSlug}</small>
|
||||||
{authors()[authorSlug].name}
|
{authorEntities()[authorSlug].name}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
<For each={auth()?.info?.topics}>
|
<For each={session()?.info?.topics}>
|
||||||
{(topicSlug) => (
|
{(topicSlug) => (
|
||||||
<li>
|
<li>
|
||||||
<a href={`/author/${topicSlug}`} classList={{ unread: checkTopicIsSeen(topicSlug) }}>
|
<a href={`/author/${topicSlug}`} classList={{ unread: checkTopicIsSeen(topicSlug) }}>
|
||||||
{getTopicEntities()[topicSlug]?.title}
|
{topicEntities()[topicSlug]?.title}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,21 +3,23 @@ import { Header } from '../Nav/Header'
|
||||||
import { Footer } from '../Discours/Footer'
|
import { Footer } from '../Discours/Footer'
|
||||||
|
|
||||||
import '../../styles/app.scss'
|
import '../../styles/app.scss'
|
||||||
|
import { Show } from 'solid-js'
|
||||||
|
|
||||||
type Props = {
|
type MainLayoutProps = {
|
||||||
headerTitle?: string
|
headerTitle?: string
|
||||||
children: JSX.Element
|
children: JSX.Element
|
||||||
isHeaderFixed?: boolean
|
isHeaderFixed?: boolean
|
||||||
|
hideFooter?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MainLayout = (props: Props) => {
|
export const MainLayout = (props: MainLayoutProps) => {
|
||||||
const isHeaderFixed = props.isHeaderFixed !== undefined ? props.isHeaderFixed : true
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header title={props.headerTitle} isHeaderFixed={isHeaderFixed} />
|
<Header title={props.headerTitle} isHeaderFixed={props.isHeaderFixed === true} />
|
||||||
<main class="main-content">{props.children}</main>
|
<main class="main-content">{props.children}</main>
|
||||||
<Footer />
|
<Show when={props.hideFooter !== true}>
|
||||||
|
<Footer />
|
||||||
|
</Show>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,10 @@ import './AuthModal.scss'
|
||||||
import { Form } from 'solid-js-form'
|
import { Form } from 'solid-js-form'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { hideModal, useModalStore } from '../../stores/ui'
|
import { hideModal, useModalStore } from '../../stores/ui'
|
||||||
import { useStore } from '@nanostores/solid'
|
import { useAuthStore, signIn, register } from '../../stores/auth'
|
||||||
import { session as sessionstore, signIn } from '../../stores/auth'
|
|
||||||
import { apiClient } from '../../utils/apiClient'
|
|
||||||
import { useValidator } from '../../utils/validators'
|
import { useValidator } from '../../utils/validators'
|
||||||
import { baseUrl } from '../../graphql/publicGraphQLClient'
|
import { baseUrl } from '../../graphql/publicGraphQLClient'
|
||||||
|
import { ApiError } from '../../utils/apiClient'
|
||||||
|
|
||||||
type AuthMode = 'sign-in' | 'sign-up' | 'forget' | 'reset' | 'resend' | 'password'
|
type AuthMode = 'sign-in' | 'sign-up' | 'forget' | 'reset' | 'resend' | 'password'
|
||||||
|
|
||||||
|
@ -23,7 +22,7 @@ const statuses: { [key: string]: string } = {
|
||||||
const titles = {
|
const titles = {
|
||||||
'sign-up': t('Create account'),
|
'sign-up': t('Create account'),
|
||||||
'sign-in': t('Enter the Discours'),
|
'sign-in': t('Enter the Discours'),
|
||||||
forget: t('Forget password?'),
|
forget: t('Forgot password?'),
|
||||||
reset: t('Please, confirm your email to finish'),
|
reset: t('Please, confirm your email to finish'),
|
||||||
resend: t('Resend code'),
|
resend: t('Resend code'),
|
||||||
password: t('Enter your new password')
|
password: t('Enter your new password')
|
||||||
|
@ -34,10 +33,10 @@ const titles = {
|
||||||
// FIXME !!!
|
// FIXME !!!
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
export default (props: { code?: string; mode?: string }) => {
|
export default (props: { code?: string; mode?: string }) => {
|
||||||
const session = useStore(sessionstore)
|
const { session } = useAuthStore()
|
||||||
const [handshaking] = createSignal(false)
|
const [handshaking] = createSignal(false)
|
||||||
const { getModal } = useModalStore()
|
const { getModal } = useModalStore()
|
||||||
const [authError, setError] = createSignal('')
|
const [authError, setError] = createSignal<string>('')
|
||||||
const [mode, setMode] = createSignal<AuthMode>('sign-in')
|
const [mode, setMode] = createSignal<AuthMode>('sign-in')
|
||||||
const [validation, setValidation] = createSignal({})
|
const [validation, setValidation] = createSignal({})
|
||||||
const [initial, setInitial] = createSignal({})
|
const [initial, setInitial] = createSignal({})
|
||||||
|
@ -46,7 +45,7 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
let passElement: HTMLInputElement | undefined
|
let passElement: HTMLInputElement | undefined
|
||||||
let codeElement: HTMLInputElement | undefined
|
let codeElement: HTMLInputElement | undefined
|
||||||
|
|
||||||
// 3rd party providier auth handler
|
// 3rd party provider auth handler
|
||||||
const oauth = (provider: string): void => {
|
const oauth = (provider: string): void => {
|
||||||
const popup = window.open(`${baseUrl}/oauth/${provider}`, provider, 'width=740, height=420')
|
const popup = window.open(`${baseUrl}/oauth/${provider}`, provider, 'width=740, height=420')
|
||||||
popup?.focus()
|
popup?.focus()
|
||||||
|
@ -85,28 +84,50 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
setValidation(vs)
|
setValidation(vs)
|
||||||
setInitial(ini)
|
setInitial(ini)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(setupValidators)
|
onMount(setupValidators)
|
||||||
|
|
||||||
|
const resetError = () => {
|
||||||
|
setError('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeMode = (newMode: AuthMode) => {
|
||||||
|
setMode(newMode)
|
||||||
|
resetError()
|
||||||
|
}
|
||||||
|
|
||||||
// local auth handler
|
// local auth handler
|
||||||
const localAuth = async () => {
|
const localAuth = async () => {
|
||||||
console.log('[auth] native account processing')
|
console.log('[auth] native account processing')
|
||||||
switch (mode()) {
|
switch (mode()) {
|
||||||
case 'sign-in':
|
case 'sign-in':
|
||||||
signIn({ email: emailElement?.value, password: passElement?.value })
|
try {
|
||||||
|
await signIn({ email: emailElement?.value, password: passElement?.value })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError) {
|
||||||
|
if (error.code === 'email_not_confirmed') {
|
||||||
|
setError(t('Please, confirm email'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'user_not_found') {
|
||||||
|
setError(t('Something went wrong, check email and password'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
case 'sign-up':
|
case 'sign-up':
|
||||||
if (pass2Element?.value !== passElement?.value) {
|
if (pass2Element?.value !== passElement?.value) {
|
||||||
setError(t('Passwords are not equal'))
|
setError(t('Passwords are not equal'))
|
||||||
} else {
|
} else {
|
||||||
// FIXME use store actions
|
await register({
|
||||||
const r = await apiClient.authRegiser({
|
|
||||||
email: emailElement?.value,
|
email: emailElement?.value,
|
||||||
password: passElement?.value
|
password: passElement?.value
|
||||||
})
|
})
|
||||||
if (r) {
|
|
||||||
console.debug('[auth] session update', r)
|
|
||||||
sessionstore.set(r)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'reset':
|
case 'reset':
|
||||||
|
@ -130,6 +151,7 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME move to handlers
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (session()?.user?.slug && getModal() === 'auth') {
|
if (session()?.user?.slug && getModal() === 'auth') {
|
||||||
// hiding itself if finished
|
// hiding itself if finished
|
||||||
|
@ -141,7 +163,8 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
} else {
|
} else {
|
||||||
console.log('[auth] session', session())
|
console.log('[auth] session', session())
|
||||||
}
|
}
|
||||||
}, [session()])
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="row view" classList={{ 'view--sign-up': mode() === 'sign-up' }}>
|
<div class="row view" classList={{ 'view--sign-up': mode() === 'sign-up' }}>
|
||||||
<div class="col-sm-6 d-md-none auth-image">
|
<div class="col-sm-6 d-md-none auth-image">
|
||||||
|
@ -174,7 +197,6 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
>
|
>
|
||||||
<div class="auth__inner">
|
<div class="auth__inner">
|
||||||
<h4>{titles[mode()]}</h4>
|
<h4>{titles[mode()]}</h4>
|
||||||
|
|
||||||
<div class={`auth-subtitle ${mode() === 'forget' ? '' : 'hidden'}`}>
|
<div class={`auth-subtitle ${mode() === 'forget' ? '' : 'hidden'}`}>
|
||||||
<Show
|
<Show
|
||||||
when={mode() === 'forget'}
|
when={mode() === 'forget'}
|
||||||
|
@ -187,7 +209,6 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
{t('Everything is ok, please give us your email address')}
|
{t('Everything is ok, please give us your email address')}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={authError()}>
|
<Show when={authError()}>
|
||||||
<div class={`auth-info`}>
|
<div class={`auth-info`}>
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -195,7 +216,6 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/*FIXME*/}
|
{/*FIXME*/}
|
||||||
{/*<Show when={false && mode() === 'sign-up'}>*/}
|
{/*<Show when={false && mode() === 'sign-up'}>*/}
|
||||||
{/* <div class='pretty-form__item'>*/}
|
{/* <div class='pretty-form__item'>*/}
|
||||||
|
@ -223,7 +243,6 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
<label for="email">{t('Email')}</label>
|
<label for="email">{t('Email')}</label>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={mode() === 'sign-up' || mode() === 'sign-in' || mode() === 'password'}>
|
<Show when={mode() === 'sign-up' || mode() === 'sign-in' || mode() === 'password'}>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
|
@ -250,7 +269,6 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
<label for="resetcode">{t('Reset code')}</label>
|
<label for="resetcode">{t('Reset code')}</label>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={mode() === 'password' || mode() === 'sign-up'}>
|
<Show when={mode() === 'password' || mode() === 'sign-up'}>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
|
@ -269,15 +287,19 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
{handshaking() ? '...' : titles[mode()]}
|
{handshaking() ? '...' : titles[mode()]}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={mode() === 'sign-in'}>
|
<Show when={mode() === 'sign-in'}>
|
||||||
<div class="auth-actions">
|
<div class="auth-actions">
|
||||||
<a href={''} onClick={() => setMode('forget')}>
|
<a
|
||||||
{t('Forget password?')}
|
href="#"
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.preventDefault()
|
||||||
|
changeMode('forget')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Forgot password?')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={mode() === 'sign-in' || mode() === 'sign-up'}>
|
<Show when={mode() === 'sign-in' || mode() === 'sign-up'}>
|
||||||
<div class="social-provider">
|
<div class="social-provider">
|
||||||
<div class="providers-text">{t('Or continue with social network')}</div>
|
<div class="providers-text">{t('Or continue with social network')}</div>
|
||||||
|
@ -297,25 +319,24 @@ export default (props: { code?: string; mode?: string }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="auth-control">
|
<div class="auth-control">
|
||||||
<div classList={{ show: mode() === 'sign-up' }}>
|
<div classList={{ show: mode() === 'sign-up' }}>
|
||||||
<span class="auth-link" onClick={() => setMode('sign-in')}>
|
<span class="auth-link" onClick={() => changeMode('sign-in')}>
|
||||||
{t('I have an account')}
|
{t('I have an account')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div classList={{ show: mode() === 'sign-in' }}>
|
<div classList={{ show: mode() === 'sign-in' }}>
|
||||||
<span class="auth-link" onClick={() => setMode('sign-up')}>
|
<span class="auth-link" onClick={() => changeMode('sign-up')}>
|
||||||
{t('I have no account yet')}
|
{t('I have no account yet')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div classList={{ show: mode() === 'forget' }}>
|
<div classList={{ show: mode() === 'forget' }}>
|
||||||
<span class="auth-link" onClick={() => setMode('sign-in')}>
|
<span class="auth-link" onClick={() => changeMode('sign-in')}>
|
||||||
{t('I know the password')}
|
{t('I know the password')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div classList={{ show: mode() === 'reset' }}>
|
<div classList={{ show: mode() === 'reset' }}>
|
||||||
<span class="auth-link" onClick={() => setMode('resend')}>
|
<span class="auth-link" onClick={() => changeMode('resend')}>
|
||||||
{t('Resend code')}
|
{t('Resend code')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,8 +6,7 @@ import { Modal } from './Modal'
|
||||||
import AuthModal from './AuthModal'
|
import AuthModal from './AuthModal'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
||||||
import { useStore } from '@nanostores/solid'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { session as ssession } from '../../stores/auth'
|
|
||||||
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||||
import './Header.scss'
|
import './Header.scss'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
|
@ -38,7 +37,7 @@ export const Header = (props: Props) => {
|
||||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
||||||
// stores
|
// stores
|
||||||
const { getWarnings } = useWarningsStore()
|
const { getWarnings } = useWarningsStore()
|
||||||
const session = useStore(ssession)
|
const { session } = useAuthStore()
|
||||||
const { getModal } = useModalStore()
|
const { getModal } = useModalStore()
|
||||||
|
|
||||||
const { getPage } = useRouter()
|
const { getPage } = useRouter()
|
||||||
|
|
|
@ -2,12 +2,11 @@ import type { Author } from '../../graphql/types.gen'
|
||||||
import Userpic from '../Author/Userpic'
|
import Userpic from '../Author/Userpic'
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import './Private.scss'
|
import './Private.scss'
|
||||||
import { session as sesstore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const session = useStore(sesstore)
|
const { session } = useAuthStore()
|
||||||
const { getPage } = useRouter()
|
const { getPage } = useRouter()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { AuthorCard } from '../Author/Card'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { hideModal } from '../../stores/ui'
|
import { hideModal } from '../../stores/ui'
|
||||||
import { session, signOut } from '../../stores/auth'
|
import { useAuthStore, signOut } from '../../stores/auth'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { createMemo } from 'solid-js'
|
import { createMemo } from 'solid-js'
|
||||||
|
|
||||||
const quit = () => {
|
const quit = () => {
|
||||||
|
@ -13,29 +12,32 @@ const quit = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const auth = useStore(session)
|
const { session } = useAuthStore()
|
||||||
const author = createMemo(() => {
|
|
||||||
const a = {
|
const author = createMemo<Author>(() => {
|
||||||
|
const a: Author = {
|
||||||
name: 'anonymous',
|
name: 'anonymous',
|
||||||
userpic: '',
|
userpic: '',
|
||||||
slug: ''
|
slug: ''
|
||||||
} as Author
|
}
|
||||||
if (auth()?.user?.slug) {
|
|
||||||
const u = auth().user
|
if (session()?.user?.slug) {
|
||||||
|
const u = session().user
|
||||||
a.name = u.name
|
a.name = u.name
|
||||||
a.slug = u.slug
|
a.slug = u.slug
|
||||||
a.userpic = u.userpic
|
a.userpic = u.userpic
|
||||||
}
|
}
|
||||||
|
|
||||||
return a
|
return a
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: ProfileModal markup and styles
|
// TODO: ProfileModal markup and styles
|
||||||
return (
|
return (
|
||||||
<div class="row view profile">
|
<div class="row view profile">
|
||||||
<h1>{auth()?.user?.username}</h1>
|
<h1>{session()?.user?.username}</h1>
|
||||||
<AuthorCard author={author()} />
|
<AuthorCard author={author()} />
|
||||||
<div class="profile-bio">{auth()?.user?.bio || ''}</div>
|
<div class="profile-bio">{session()?.user?.bio || ''}</div>
|
||||||
<For each={auth()?.user?.links || []}>{(l: string) => <a href={l}>{l}</a>}</For>
|
<For each={session()?.user?.links || []}>{(l: string) => <a href={l}>{l}</a>}</For>
|
||||||
<span onClick={quit}>{t('Quit')}</span>
|
<span onClick={quit}>{t('Quit')}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -42,7 +42,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
font-weight: 500;
|
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
|
|
||||||
export const NavTopics = (props: { topics: Topic[] }) => {
|
export const NavTopics = (props: { topics: Topic[] }) => {
|
||||||
const tag = (t: Topic) => (/[ЁА-яё]/.test(t.title || '') && locale() !== 'ru' ? t.slug : t.title)
|
const tag = (topic: Topic) =>
|
||||||
|
/[ЁА-яё]/.test(topic.title || '') && locale() !== 'ru' ? topic.slug : topic.title
|
||||||
|
|
||||||
// TODO: something about subtopics
|
// TODO: something about subtopics
|
||||||
return (
|
return (
|
||||||
|
@ -14,10 +15,10 @@ export const NavTopics = (props: { topics: Topic[] }) => {
|
||||||
<ul class="topics">
|
<ul class="topics">
|
||||||
<Show when={props.topics.length > 0}>
|
<Show when={props.topics.length > 0}>
|
||||||
<For each={props.topics}>
|
<For each={props.topics}>
|
||||||
{(t: Topic) => (
|
{(topic) => (
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<a href={`/topic/${t.slug}`}>
|
<a href={`/topic/${topic.slug}`}>
|
||||||
<span>#{tag(t)}</span>
|
<span>#{tag(topic)}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -18,17 +18,17 @@ export const ArticlePage = (props: PageProps) => {
|
||||||
throw new Error('ts guard')
|
throw new Error('ts guard')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getArticleEntities } = useArticlesStore({
|
const { articleEntities } = useArticlesStore({
|
||||||
sortedArticles
|
sortedArticles
|
||||||
})
|
})
|
||||||
|
|
||||||
const article = createMemo<Shout>(() => getArticleEntities()[page.params.slug])
|
const article = createMemo<Shout>(() => articleEntities()[page.params.slug])
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const slug = page.params.slug
|
const slug = page.params.slug
|
||||||
const article = getArticleEntities()[slug]
|
const articleValue = articleEntities()[slug]
|
||||||
|
|
||||||
if (!article || !article.body) {
|
if (!articleValue || !articleValue.body) {
|
||||||
loadArticle({ slug })
|
loadArticle({ slug })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { MainLayout } from '../Layouts/MainLayout'
|
||||||
|
|
||||||
export const FourOuFourPage = () => {
|
export const FourOuFourPage = () => {
|
||||||
return (
|
return (
|
||||||
<MainLayout isHeaderFixed={false}>
|
<MainLayout isHeaderFixed={false} hideFooter={true}>
|
||||||
<FourOuFourView />
|
<FourOuFourView />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,43 +2,49 @@
|
||||||
// import 'solid-devtools'
|
// import 'solid-devtools'
|
||||||
|
|
||||||
import { setLocale } from '../stores/ui'
|
import { setLocale } from '../stores/ui'
|
||||||
import { Component, createEffect, createMemo, lazy } from 'solid-js'
|
import { Component, createEffect, createMemo } from 'solid-js'
|
||||||
import { Routes, useRouter } from '../stores/router'
|
import { Routes, useRouter } from '../stores/router'
|
||||||
import { Dynamic, isServer } from 'solid-js/web'
|
import { Dynamic, isServer } from 'solid-js/web'
|
||||||
import { getLogger } from '../utils/logger'
|
import { getLogger } from '../utils/logger'
|
||||||
|
|
||||||
import type { PageProps } from './types'
|
import type { PageProps } from './types'
|
||||||
|
|
||||||
// do not remove
|
import { HomePage } from './Pages/HomePage'
|
||||||
// for debugging, to disable lazy loading
|
import { AllTopicsPage } from './Pages/AllTopicsPage'
|
||||||
// import HomePage from './Pages/HomePage'
|
import { TopicPage } from './Pages/TopicPage'
|
||||||
// import AllTopicsPage from './Pages/AllTopicsPage'
|
import { AllAuthorsPage } from './Pages/AllAuthorsPage'
|
||||||
// import TopicPage from './Pages/TopicPage'
|
import { AuthorPage } from './Pages/AuthorPage'
|
||||||
// import AllAuthorsPage from './Pages/AllAuthorsPage'
|
import { FeedPage } from './Pages/FeedPage'
|
||||||
// import AuthorPage from './Pages/AuthorPage'
|
import { ArticlePage } from './Pages/ArticlePage'
|
||||||
// import FeedPage from './Pages/FeedPage'
|
import { SearchPage } from './Pages/SearchPage'
|
||||||
// import ArticlePage from './Pages/ArticlePage'
|
import { FourOuFourPage } from './Pages/FourOuFourPage'
|
||||||
// import SearchPage from './Pages/SearchPage'
|
import { DogmaPage } from './Pages/about/DogmaPage'
|
||||||
// import FourOuFourPage from './Pages/FourOuFourPage'
|
import { GuidePage } from './Pages/about/GuidePage'
|
||||||
|
import { HelpPage } from './Pages/about/HelpPage'
|
||||||
|
import { ManifestPage } from './Pages/about/ManifestPage'
|
||||||
|
import { PartnersPage } from './Pages/about/PartnersPage'
|
||||||
|
import { ProjectsPage } from './Pages/about/ProjectsPage'
|
||||||
|
import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
||||||
|
import { ThanksPage } from './Pages/about/ThanksPage'
|
||||||
|
|
||||||
const HomePage = lazy(() => import('./Pages/HomePage'))
|
// TODO: lazy load
|
||||||
const AllTopicsPage = lazy(() => import('./Pages/AllTopicsPage'))
|
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
||||||
const TopicPage = lazy(() => import('./Pages/TopicPage'))
|
// const AllTopicsPage = lazy(() => import('./Pages/AllTopicsPage'))
|
||||||
const AllAuthorsPage = lazy(() => import('./Pages/AllAuthorsPage'))
|
// const TopicPage = lazy(() => import('./Pages/TopicPage'))
|
||||||
const AuthorPage = lazy(() => import('./Pages/AuthorPage'))
|
// const AllAuthorsPage = lazy(() => import('./Pages/AllAuthorsPage'))
|
||||||
const FeedPage = lazy(() => import('./Pages/FeedPage'))
|
// const AuthorPage = lazy(() => import('./Pages/AuthorPage'))
|
||||||
const ArticlePage = lazy(() => import('./Pages/ArticlePage'))
|
// const FeedPage = lazy(() => import('./Pages/FeedPage'))
|
||||||
const SearchPage = lazy(() => import('./Pages/SearchPage'))
|
// const ArticlePage = lazy(() => import('./Pages/ArticlePage'))
|
||||||
const FourOuFourPage = lazy(() => import('./Pages/FourOuFourPage'))
|
// const SearchPage = lazy(() => import('./Pages/SearchPage'))
|
||||||
const DogmaPage = lazy(() => import('./Pages/about/DogmaPage'))
|
// const FourOuFourPage = lazy(() => import('./Pages/FourOuFourPage'))
|
||||||
|
// const DogmaPage = lazy(() => import('./Pages/about/DogmaPage'))
|
||||||
const GuidePage = lazy(() => import('./Pages/about/GuidePage'))
|
// const GuidePage = lazy(() => import('./Pages/about/GuidePage'))
|
||||||
const HelpPage = lazy(() => import('./Pages/about/HelpPage'))
|
// const HelpPage = lazy(() => import('./Pages/about/HelpPage'))
|
||||||
const ManifestPage = lazy(() => import('./Pages/about/ManifestPage'))
|
// const ManifestPage = lazy(() => import('./Pages/about/ManifestPage'))
|
||||||
const PartnersPage = lazy(() => import('./Pages/about/PartnersPage'))
|
// const PartnersPage = lazy(() => import('./Pages/about/PartnersPage'))
|
||||||
const ProjectsPage = lazy(() => import('./Pages/about/ProjectsPage'))
|
// const ProjectsPage = lazy(() => import('./Pages/about/ProjectsPage'))
|
||||||
const TermsOfUsePage = lazy(() => import('./Pages/about/TermsOfUsePage'))
|
// const TermsOfUsePage = lazy(() => import('./Pages/about/TermsOfUsePage'))
|
||||||
const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
||||||
|
|
||||||
const log = getLogger('root')
|
const log = getLogger('root')
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,11 @@ import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { useStore } from '@nanostores/solid'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { session } from '../../stores/auth'
|
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
|
|
||||||
|
const log = getLogger('TopicCard')
|
||||||
|
|
||||||
interface TopicProps {
|
interface TopicProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
|
@ -19,63 +21,73 @@ interface TopicProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicCard = (props: TopicProps) => {
|
export const TopicCard = (props: TopicProps) => {
|
||||||
const auth = useStore(session)
|
const { session } = useAuthStore()
|
||||||
const topic = createMemo(() => props.topic)
|
|
||||||
const subscribed = createMemo(() => {
|
const subscribed = createMemo(() => {
|
||||||
return Boolean(auth()?.user?.slug) && topic().slug ? topic().slug in auth().info.topics : false
|
if (!session()?.user?.slug || !session()?.info?.topics) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.topic.slug in session().info.topics
|
||||||
})
|
})
|
||||||
|
|
||||||
// FIXME use store actions
|
// FIXME use store actions
|
||||||
const subscribe = async (really = true) => {
|
const subscribe = async (really = true) => {
|
||||||
if (really) {
|
if (really) {
|
||||||
follow({ what: FollowingEntity.Topic, slug: topic().slug })
|
follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||||
} else {
|
} else {
|
||||||
unfollow({ what: FollowingEntity.Topic, slug: topic().slug })
|
unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}>
|
<div class="topic" classList={{ row: !props.compact && !props.subscribeButtonBottom }}>
|
||||||
<div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}>
|
<div classList={{ 'col-md-7': !props.compact && !props.subscribeButtonBottom }}>
|
||||||
<Show when={topic().title}>
|
<Show when={props.topic.title}>
|
||||||
<div class="topic-title">
|
<div class="topic-title">
|
||||||
<a href={`/topic/${topic().slug}`}>{capitalize(topic().title || '')}</a>
|
<a href={`/topic/${props.topic.slug}`}>{capitalize(props.topic.title || '')}</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={topic().pic}>
|
<Show when={props.topic.pic}>
|
||||||
<div class="topic__avatar">
|
<div class="topic__avatar">
|
||||||
<a href={topic().slug}>
|
<a href={props.topic.slug}>
|
||||||
<img src={topic().pic} alt={topic().title} />
|
<img src={props.topic.pic} alt={props.topic.title} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!props.compact && topic()?.body}>
|
<Show when={!props.compact && props.topic?.body}>
|
||||||
<div class="topic-description" classList={{ 'topic-description--short': props.shortDescription }}>
|
<div class="topic-description" classList={{ 'topic-description--short': props.shortDescription }}>
|
||||||
{topic().body}
|
{props.topic.body}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={topic()?.stat}>
|
<Show when={props.topic?.stat}>
|
||||||
<div class="topic-details">
|
<div class="topic-details">
|
||||||
<Show when={!props.compact}>
|
<Show when={!props.compact}>
|
||||||
<span class="topic-details__item" classList={{ compact: props.compact }}>
|
<span class="topic-details__item" classList={{ compact: props.compact }}>
|
||||||
{topic().stat?.shouts +
|
{props.topic.stat?.shouts +
|
||||||
' ' +
|
' ' +
|
||||||
t('post') +
|
t('post') +
|
||||||
plural(topic().stat?.shouts || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])}
|
plural(
|
||||||
|
props.topic.stat?.shouts || 0,
|
||||||
|
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span class="topic-details__item" classList={{ compact: props.compact }}>
|
<span class="topic-details__item" classList={{ compact: props.compact }}>
|
||||||
{topic().stat?.authors +
|
{props.topic.stat?.authors +
|
||||||
' ' +
|
' ' +
|
||||||
t('author') +
|
t('author') +
|
||||||
plural(topic().stat?.authors || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])}
|
plural(
|
||||||
|
props.topic.stat?.authors || 0,
|
||||||
|
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span class="topic-details__item" classList={{ compact: props.compact }}>
|
<span class="topic-details__item" classList={{ compact: props.compact }}>
|
||||||
{topic().stat?.followers +
|
{props.topic.stat?.followers +
|
||||||
' ' +
|
' ' +
|
||||||
t('follower') +
|
t('follower') +
|
||||||
plural(
|
plural(
|
||||||
topic().stat?.followers || 0,
|
props.topic.stat?.followers || 0,
|
||||||
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
|
locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's']
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -3,8 +3,7 @@ import { Show } from 'solid-js/web'
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
import { session } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
|
||||||
|
@ -13,8 +12,9 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullTopic = (props: Props) => {
|
export const FullTopic = (props: Props) => {
|
||||||
const auth = useStore(session)
|
const { session } = useAuthStore()
|
||||||
const subscribed = createMemo(() => auth()?.info?.topics?.includes(props.topic?.slug))
|
|
||||||
|
const subscribed = createMemo(() => session()?.info?.topics?.includes(props.topic?.slug))
|
||||||
return (
|
return (
|
||||||
<div class="topic-full container">
|
<div class="topic-full container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -7,8 +7,7 @@ import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { session } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import '../../styles/AllTopics.scss'
|
import '../../styles/AllTopics.scss'
|
||||||
|
|
||||||
type AllAuthorsPageSearchParams = {
|
type AllAuthorsPageSearchParams = {
|
||||||
|
@ -20,19 +19,21 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AllAuthorsView = (props: Props) => {
|
export const AllAuthorsView = (props: Props) => {
|
||||||
const { getSortedAuthors: authorslist } = useAuthorsStore({ authors: props.authors })
|
const { sortedAuthors: authorList } = useAuthorsStore({ authors: props.authors })
|
||||||
const [sortedAuthors, setSortedAuthors] = createSignal<Author[]>([])
|
const [sortedAuthors, setSortedAuthors] = createSignal<Author[]>([])
|
||||||
const [sortedKeys, setSortedKeys] = createSignal<string[]>([])
|
const [sortedKeys, setSortedKeys] = createSignal<string[]>([])
|
||||||
const [abc, setAbc] = createSignal([])
|
const [abc, setAbc] = createSignal([])
|
||||||
const auth = useStore(session)
|
|
||||||
const subscribed = (s) => Boolean(auth()?.info?.authors && auth()?.info?.authors?.includes(s || ''))
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
|
const subscribed = (s) => Boolean(session()?.info?.authors && session()?.info?.authors?.includes(s || ''))
|
||||||
|
|
||||||
const { getSearchParams } = useRouter<AllAuthorsPageSearchParams>()
|
const { getSearchParams } = useRouter<AllAuthorsPageSearchParams>()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if ((!getSearchParams().by || getSearchParams().by === 'name') && abc().length === 0) {
|
if ((!getSearchParams().by || getSearchParams().by === 'name') && abc().length === 0) {
|
||||||
console.log('[authors] default grouping by abc')
|
console.log('[authors] default grouping by abc')
|
||||||
const grouped = { ...groupByName(authorslist()) }
|
const grouped = { ...groupByName(authorList()) }
|
||||||
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
|
grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar)
|
||||||
setAbc(grouped)
|
setAbc(grouped)
|
||||||
const keys = Object.keys(abc)
|
const keys = Object.keys(abc)
|
||||||
|
@ -40,13 +41,13 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
setSortedKeys(keys as string[])
|
setSortedKeys(keys as string[])
|
||||||
} else {
|
} else {
|
||||||
console.log('[authors] sorting by ' + getSearchParams().by)
|
console.log('[authors] sorting by ' + getSearchParams().by)
|
||||||
setSortedAuthors(sortBy(authorslist(), getSearchParams().by))
|
setSortedAuthors(sortBy(authorList(), getSearchParams().by))
|
||||||
}
|
}
|
||||||
}, [authorslist(), getSearchParams().by])
|
}, [authorList(), getSearchParams().by])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="all-topics-page">
|
<div class="all-topics-page">
|
||||||
<Show when={sortedAuthors()}>
|
<Show when={sortedAuthors().length > 0}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="shift-content">
|
<div class="shift-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -2,39 +2,42 @@ import { createEffect, For, Show } from 'solid-js'
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { setSortAllTopicsBy, useTopicsStore } from '../../stores/zine/topics'
|
import { setSortAllBy as setSortAllTopicsBy, useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
import { session } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import '../../styles/AllTopics.scss'
|
import '../../styles/AllTopics.scss'
|
||||||
|
import { getLogger } from '../../utils/logger'
|
||||||
|
|
||||||
|
const log = getLogger('AllTopicsView')
|
||||||
|
|
||||||
type AllTopicsPageSearchParams = {
|
type AllTopicsPageSearchParams = {
|
||||||
by: 'shouts' | 'authors' | 'title' | ''
|
by: 'shouts' | 'authors' | 'title' | ''
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type AllTopicsViewProps = {
|
||||||
topics: Topic[]
|
topics: Topic[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AllTopicsView = (props: Props) => {
|
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
const { getSearchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
const { getSearchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||||
|
|
||||||
const { getSortedTopics } = useTopicsStore({
|
const { sortedTopics } = useTopicsStore({
|
||||||
topics: props.topics,
|
topics: props.topics,
|
||||||
sortBy: getSearchParams().by || 'shouts'
|
sortBy: getSearchParams().by || 'shouts'
|
||||||
})
|
})
|
||||||
const auth = useStore(session)
|
|
||||||
|
const { session } = useAuthStore()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setSortAllTopicsBy(getSearchParams().by || 'shouts')
|
setSortAllTopicsBy(getSearchParams().by || 'shouts')
|
||||||
})
|
})
|
||||||
|
|
||||||
const subscribed = (s) => Boolean(auth()?.info?.topics && auth()?.info?.topics?.includes(s || ''))
|
const subscribed = (s) => Boolean(session()?.info?.topics && session()?.info?.topics?.includes(s || ''))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="all-topics-page">
|
<div class="all-topics-page">
|
||||||
<Show when={getSortedTopics().length > 0}>
|
<Show when={sortedTopics().length > 0}>
|
||||||
<div class="wide-container">
|
<div class="wide-container">
|
||||||
<div class="shift-content">
|
<div class="shift-content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -78,7 +81,7 @@ export const AllTopicsView = (props: Props) => {
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<For each={getSortedTopics()}>
|
<For each={sortedTopics()}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
|
<TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -25,12 +25,12 @@ type AuthorPageSearchParams = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorView = (props: AuthorProps) => {
|
export const AuthorView = (props: AuthorProps) => {
|
||||||
const { getSortedArticles: articles } = useArticlesStore({
|
const { sortedArticles } = useArticlesStore({
|
||||||
sortedArticles: props.authorArticles
|
sortedArticles: props.authorArticles
|
||||||
})
|
})
|
||||||
const { getAuthorEntities: authors } = useAuthorsStore({ authors: [props.author] })
|
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||||
|
|
||||||
const author = createMemo(() => authors()[props.author.slug])
|
const author = createMemo(() => authorEntities()[props.author.slug])
|
||||||
const { getSearchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
const { getSearchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
|
||||||
|
|
||||||
//const slug = createMemo(() => author().slug)
|
//const slug = createMemo(() => author().slug)
|
||||||
|
@ -90,7 +90,7 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
<div class="floor">
|
<div class="floor">
|
||||||
<h3 class="col-12">{title()}</h3>
|
<h3 class="col-12">{title()}</h3>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<Show when={articles()?.length > 0}>
|
<Show when={sortedArticles().length > 0}>
|
||||||
{/*FIXME*/}
|
{/*FIXME*/}
|
||||||
{/*<Beside*/}
|
{/*<Beside*/}
|
||||||
{/* title={t('Topics which supported by author')}*/}
|
{/* title={t('Topics which supported by author')}*/}
|
||||||
|
@ -99,10 +99,10 @@ export const AuthorView = (props: AuthorProps) => {
|
||||||
{/* wrapper={'topic'}*/}
|
{/* wrapper={'topic'}*/}
|
||||||
{/* topicShortDescription={true}*/}
|
{/* topicShortDescription={true}*/}
|
||||||
{/*/>*/}
|
{/*/>*/}
|
||||||
<Row3 articles={articles().slice(1, 4)} />
|
<Row3 articles={sortedArticles().slice(1, 4)} />
|
||||||
<Row2 articles={articles().slice(4, 6)} />
|
<Row2 articles={sortedArticles().slice(4, 6)} />
|
||||||
<Row3 articles={articles().slice(10, 13)} />
|
<Row3 articles={sortedArticles().slice(10, 13)} />
|
||||||
<Row3 articles={articles().slice(13, 16)} />
|
<Row3 articles={sortedArticles().slice(13, 16)} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,9 +7,8 @@ import { TopicCard } from '../Topic/Card'
|
||||||
import { ArticleCard } from '../Feed/Card'
|
import { ArticleCard } from '../Feed/Card'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { FeedSidebar } from '../Feed/Sidebar'
|
import { FeedSidebar } from '../Feed/Sidebar'
|
||||||
import { session } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import CommentCard from '../Article/Comment'
|
import CommentCard from '../Article/Comment'
|
||||||
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
|
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||||
|
@ -31,13 +30,12 @@ interface FeedProps {
|
||||||
|
|
||||||
export const FeedView = (props: FeedProps) => {
|
export const FeedView = (props: FeedProps) => {
|
||||||
// state
|
// state
|
||||||
const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.articles })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.articles })
|
||||||
const reactions = useReactionsStore()
|
const reactions = useReactionsStore()
|
||||||
const { getSortedAuthors: authors } = useAuthorsStore()
|
const { sortedAuthors } = useAuthorsStore()
|
||||||
const { getTopTopics } = useTopicsStore()
|
const { topTopics } = useTopicsStore()
|
||||||
const { getTopAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
|
const { session } = useAuthStore()
|
||||||
const auth = useStore(session)
|
|
||||||
|
|
||||||
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
||||||
|
|
||||||
|
@ -66,12 +64,12 @@ export const FeedView = (props: FeedProps) => {
|
||||||
<div class="container feed">
|
<div class="container feed">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-3 feed-navigation">
|
<div class="col-md-3 feed-navigation">
|
||||||
<FeedSidebar authors={authors()} />
|
<FeedSidebar authors={sortedAuthors()} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<ul class="feed-filter">
|
<ul class="feed-filter">
|
||||||
<Show when={!!auth()?.user?.slug}>
|
<Show when={!!session()?.user?.slug}>
|
||||||
<li class="selected">
|
<li class="selected">
|
||||||
<a href="/feed/my">{t('My feed')}</a>
|
<a href="/feed/my">{t('My feed')}</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -87,8 +85,8 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Show when={articles().length > 0}>
|
<Show when={sortedArticles().length > 0}>
|
||||||
<For each={articles().slice(0, 4)}>
|
<For each={sortedArticles().slice(0, 4)}>
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</For>
|
</For>
|
||||||
|
|
||||||
|
@ -101,7 +99,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="beside-column">
|
<ul class="beside-column">
|
||||||
<For each={getTopAuthors().slice(0, 5)}>
|
<For each={topAuthors().slice(0, 5)}>
|
||||||
{(author) => (
|
{(author) => (
|
||||||
<li>
|
<li>
|
||||||
<AuthorCard author={author} compact={true} hasLink={true} />
|
<AuthorCard author={author} compact={true} hasLink={true} />
|
||||||
|
@ -110,7 +108,7 @@ export const FeedView = (props: FeedProps) => {
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<For each={articles().slice(4)}>
|
<For each={sortedArticles().slice(4)}>
|
||||||
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -127,10 +125,10 @@ export const FeedView = (props: FeedProps) => {
|
||||||
{(comment) => <CommentCard comment={comment} compact={true} />}
|
{(comment) => <CommentCard comment={comment} compact={true} />}
|
||||||
</For>
|
</For>
|
||||||
</section>
|
</section>
|
||||||
<Show when={getTopTopics().length > 0}>
|
<Show when={topTopics().length > 0}>
|
||||||
<section class="feed-topics">
|
<section class="feed-topics">
|
||||||
<h4>{t('Topics')}</h4>
|
<h4>{t('Topics')}</h4>
|
||||||
<For each={getTopTopics().slice(0, 5)}>
|
<For each={topTopics().slice(0, 5)}>
|
||||||
{(topic) => <TopicCard topic={topic} subscribeButtonBottom={true} />}
|
{(topic) => <TopicCard topic={topic} subscribeButtonBottom={true} />}
|
||||||
</For>
|
</For>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import styles from '../../styles/FourOuFour.module.scss'
|
import styles from '../../styles/FourOuFour.module.scss'
|
||||||
import clsx from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
|
||||||
export const FourOuFourView = (_props) => {
|
export const FourOuFourView = (_props) => {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
} from '../../stores/zine/articles'
|
} from '../../stores/zine/articles'
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
|
||||||
const log = getLogger('home view')
|
const log = getLogger('home view')
|
||||||
|
|
||||||
|
@ -30,50 +31,51 @@ type HomeProps = {
|
||||||
randomTopics: Topic[]
|
randomTopics: Topic[]
|
||||||
recentPublishedArticles: Shout[]
|
recentPublishedArticles: Shout[]
|
||||||
}
|
}
|
||||||
|
const PRERENDERED_ARTICLES_COUNT = 5
|
||||||
const CLIENT_LOAD_ARTICLES_COUNT = 30
|
const CLIENT_LOAD_ARTICLES_COUNT = 29
|
||||||
const LOAD_MORE_ARTICLES_COUNT = 30
|
const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3
|
||||||
|
|
||||||
export const HomeView = (props: HomeProps) => {
|
export const HomeView = (props: HomeProps) => {
|
||||||
const {
|
const {
|
||||||
getSortedArticles,
|
sortedArticles,
|
||||||
getTopArticles,
|
topArticles,
|
||||||
getTopMonthArticles,
|
topMonthArticles,
|
||||||
getTopViewedArticles,
|
topViewedArticles,
|
||||||
getTopCommentedArticles,
|
topCommentedArticles,
|
||||||
getArticlesByLayout
|
articlesByLayout
|
||||||
} = useArticlesStore({
|
} = useArticlesStore({
|
||||||
sortedArticles: props.recentPublishedArticles
|
sortedArticles: props.recentPublishedArticles
|
||||||
})
|
})
|
||||||
const { getRandomTopics, getTopTopics } = useTopicsStore({
|
const { randomTopics, topTopics } = useTopicsStore({
|
||||||
randomTopics: props.randomTopics
|
randomTopics: props.randomTopics
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getTopAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadTopArticles()
|
loadTopArticles()
|
||||||
loadTopMonthArticles()
|
loadTopMonthArticles()
|
||||||
loadPublishedArticles({ limit: CLIENT_LOAD_ARTICLES_COUNT, offset: getSortedArticles().length })
|
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
||||||
|
loadPublishedArticles({ limit: CLIENT_LOAD_ARTICLES_COUNT, offset: sortedArticles().length })
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const randomLayout = createMemo(() => {
|
const randomLayout = createMemo(() => {
|
||||||
const articlesByLayout = getArticlesByLayout()
|
const filledLayouts = Object.keys(articlesByLayout()).filter(
|
||||||
const filledLayouts = Object.keys(articlesByLayout).filter(
|
|
||||||
// FIXME: is 7 ok? or more complex logic needed?
|
// FIXME: is 7 ok? or more complex logic needed?
|
||||||
(layout) => articlesByLayout[layout].length > 7
|
(layout) => articlesByLayout()[layout].length > 7
|
||||||
)
|
)
|
||||||
|
|
||||||
const randomLayout =
|
const selectedRandomLayout =
|
||||||
filledLayouts.length > 0 ? filledLayouts[Math.floor(Math.random() * filledLayouts.length)] : ''
|
filledLayouts.length > 0 ? filledLayouts[Math.floor(Math.random() * filledLayouts.length)] : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={Boolean(randomLayout)}>
|
<Show when={Boolean(selectedRandomLayout)}>
|
||||||
<Group
|
<Group
|
||||||
articles={articlesByLayout[randomLayout]}
|
articles={articlesByLayout()[selectedRandomLayout]}
|
||||||
header={
|
header={
|
||||||
<div class="layout-icon">
|
<div class="layout-icon">
|
||||||
<Icon name={randomLayout} />
|
<Icon name={selectedRandomLayout} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -81,69 +83,95 @@ export const HomeView = (props: HomeProps) => {
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadMore = () => {
|
const loadMore = async () => {
|
||||||
loadPublishedArticles({ limit: LOAD_MORE_ARTICLES_COUNT, offset: getSortedArticles().length })
|
saveScrollPosition()
|
||||||
|
await loadPublishedArticles({ limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length })
|
||||||
|
restoreScrollPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const pages = createMemo<Shout[][]>(() => {
|
||||||
<Show when={locale() && getSortedArticles().at(0) !== undefined}>
|
return sortedArticles()
|
||||||
<NavTopics topics={getRandomTopics()} />
|
.slice(PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT)
|
||||||
|
.reduce((acc, article, index) => {
|
||||||
|
if (index % LOAD_MORE_PAGE_SIZE === 0) {
|
||||||
|
acc.push([])
|
||||||
|
}
|
||||||
|
|
||||||
<Row5 articles={getSortedArticles().slice(0, 5)} />
|
acc[acc.length - 1].push(article)
|
||||||
|
return acc
|
||||||
|
}, [] as Shout[][])
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={locale()}>
|
||||||
|
<NavTopics topics={randomTopics()} />
|
||||||
|
|
||||||
|
<Row5 articles={sortedArticles().slice(0, 5)} />
|
||||||
|
|
||||||
<Hero />
|
<Hero />
|
||||||
|
|
||||||
<Beside
|
<Show when={sortedArticles().length > 5}>
|
||||||
beside={getSortedArticles().slice(4, 5)[0]}
|
|
||||||
title={t('Top viewed')}
|
|
||||||
values={getTopViewedArticles().slice(0, 5)}
|
|
||||||
wrapper={'top-article'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Row3 articles={getSortedArticles().slice(6, 9)} />
|
|
||||||
|
|
||||||
{/*FIXME: ?*/}
|
|
||||||
<Show when={getTopAuthors().length === 5}>
|
|
||||||
<Beside
|
<Beside
|
||||||
beside={getSortedArticles().slice(8, 9)[0]}
|
beside={sortedArticles()[5]}
|
||||||
|
title={t('Top viewed')}
|
||||||
|
values={topViewedArticles().slice(0, 5)}
|
||||||
|
wrapper={'top-article'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row3 articles={sortedArticles().slice(6, 9)} />
|
||||||
|
|
||||||
|
<Beside
|
||||||
|
beside={sortedArticles()[9]}
|
||||||
title={t('Top authors')}
|
title={t('Top authors')}
|
||||||
values={getTopAuthors()}
|
values={topAuthors()}
|
||||||
wrapper={'author'}
|
wrapper={'author'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Slider title={t('Top month articles')} articles={topMonthArticles()} />
|
||||||
|
|
||||||
|
<Row2 articles={sortedArticles().slice(10, 12)} />
|
||||||
|
|
||||||
|
<RowShort articles={sortedArticles().slice(12, 16)} />
|
||||||
|
|
||||||
|
<Row1 article={sortedArticles()[16]} />
|
||||||
|
<Row3 articles={sortedArticles().slice(17, 20)} />
|
||||||
|
<Row3 articles={topCommentedArticles().slice(0, 3)} header={<h2>{t('Top commented')}</h2>} />
|
||||||
|
|
||||||
|
{randomLayout()}
|
||||||
|
|
||||||
|
<Slider title={t('Favorite')} articles={topArticles()} />
|
||||||
|
|
||||||
|
<Beside
|
||||||
|
beside={sortedArticles()[20]}
|
||||||
|
title={t('Top topics')}
|
||||||
|
values={topTopics().slice(0, 5)}
|
||||||
|
wrapper={'topic'}
|
||||||
|
isTopicCompact={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row3 articles={sortedArticles().slice(21, 24)} />
|
||||||
|
|
||||||
|
<Banner />
|
||||||
|
|
||||||
|
<Row2 articles={sortedArticles().slice(24, 26)} />
|
||||||
|
<Row3 articles={sortedArticles().slice(26, 29)} />
|
||||||
|
<Row2 articles={sortedArticles().slice(29, 31)} />
|
||||||
|
<Row3 articles={sortedArticles().slice(31, 34)} />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Slider title={t('Top month articles')} articles={getTopMonthArticles()} />
|
<For each={pages()}>
|
||||||
|
{(page) => (
|
||||||
<Row2 articles={getSortedArticles().slice(10, 12)} />
|
<>
|
||||||
|
<Row1 article={page[0]} />
|
||||||
<RowShort articles={getSortedArticles().slice(12, 16)} />
|
<Row3 articles={page.slice(1, 4)} />
|
||||||
|
<Row2 articles={page.slice(4, 6)} />
|
||||||
<Row1 article={getSortedArticles().slice(15, 16)[0]} />
|
<Beside values={page.slice(6, 9)} beside={page[9]} wrapper="article" />
|
||||||
<Row3 articles={getSortedArticles().slice(17, 20)} />
|
<Row1 article={page[10]} />
|
||||||
<Row3 articles={getTopCommentedArticles()} header={<h2>{t('Top commented')}</h2>} />
|
<Row2 articles={page.slice(11, 13)} />
|
||||||
|
<Row3 articles={page.slice(13, 16)} />
|
||||||
{randomLayout()}
|
</>
|
||||||
|
)}
|
||||||
<Slider title={t('Favorite')} articles={getTopArticles()} />
|
</For>
|
||||||
|
|
||||||
<Beside
|
|
||||||
beside={getSortedArticles().slice(19, 20)[0]}
|
|
||||||
title={t('Top topics')}
|
|
||||||
values={getTopTopics().slice(0, 5)}
|
|
||||||
wrapper={'topic'}
|
|
||||||
isTopicCompact={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Row3 articles={getSortedArticles().slice(21, 24)} />
|
|
||||||
|
|
||||||
<Banner />
|
|
||||||
|
|
||||||
<Row2 articles={getSortedArticles().slice(24, 26)} />
|
|
||||||
<Row3 articles={getSortedArticles().slice(26, 29)} />
|
|
||||||
<Row2 articles={getSortedArticles().slice(29, 31)} />
|
|
||||||
<Row3 articles={getSortedArticles().slice(31, 34)} />
|
|
||||||
|
|
||||||
<For each={getSortedArticles().slice(35)}>{(article) => <Row1 article={article} />}</For>
|
|
||||||
|
|
||||||
<p class="load-more-container">
|
<p class="load-more-container">
|
||||||
<button class="button" onClick={loadMore}>
|
<button class="button" onClick={loadMore}>
|
||||||
|
|
|
@ -16,7 +16,7 @@ type Props = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchView = (props: Props) => {
|
export const SearchView = (props: Props) => {
|
||||||
const { getSortedArticles } = useArticlesStore({ sortedArticles: props.results })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.results })
|
||||||
const [getQuery, setQuery] = createSignal(props.query)
|
const [getQuery, setQuery] = createSignal(props.query)
|
||||||
|
|
||||||
const { getSearchParams } = useRouter<SearchPageSearchParams>()
|
const { getSearchParams } = useRouter<SearchPageSearchParams>()
|
||||||
|
@ -66,12 +66,12 @@ export const SearchView = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Show when={getSortedArticles().length > 0}>
|
<Show when={sortedArticles().length > 0}>
|
||||||
<h3>{t('Publications')}</h3>
|
<h3>{t('Publications')}</h3>
|
||||||
|
|
||||||
<div class="floor">
|
<div class="floor">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<For each={getSortedArticles()}>
|
<For each={sortedArticles()}>
|
||||||
{(article) => (
|
{(article) => (
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<ArticleCard article={article} />
|
<ArticleCard article={article} />
|
||||||
|
|
|
@ -24,12 +24,12 @@ interface TopicProps {
|
||||||
export const TopicView = (props: TopicProps) => {
|
export const TopicView = (props: TopicProps) => {
|
||||||
const { getSearchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
const { getSearchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
|
||||||
|
|
||||||
const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
|
||||||
const { getTopicEntities } = useTopicsStore({ topics: [props.topic] })
|
const { topicEntities } = useTopicsStore({ topics: [props.topic] })
|
||||||
|
|
||||||
const { getAuthorsByTopic } = useAuthorsStore()
|
const { authorsByTopic } = useAuthorsStore()
|
||||||
|
|
||||||
const topic = createMemo(() => getTopicEntities()[props.topic.slug])
|
const topic = createMemo(() => topicEntities()[props.topic.slug])
|
||||||
|
|
||||||
/*
|
/*
|
||||||
const slug = createMemo<string>(() => {
|
const slug = createMemo<string>(() => {
|
||||||
|
@ -104,7 +104,7 @@ export const TopicView = (props: TopicProps) => {
|
||||||
<Show when={sortedArticles().length > 5}>
|
<Show when={sortedArticles().length > 5}>
|
||||||
<Beside
|
<Beside
|
||||||
title={t('Topic is supported by')}
|
title={t('Topic is supported by')}
|
||||||
values={getAuthorsByTopic()[topic().slug].slice(0, 7)}
|
values={authorsByTopic()[topic().slug].slice(0, 7)}
|
||||||
beside={sortedArticles()[6]}
|
beside={sortedArticles()[6]}
|
||||||
wrapper={'author'}
|
wrapper={'author'}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -324,11 +324,11 @@ export type Query = {
|
||||||
getUserRoles: Array<Maybe<Role>>
|
getUserRoles: Array<Maybe<Role>>
|
||||||
getUsersBySlugs: Array<Maybe<User>>
|
getUsersBySlugs: Array<Maybe<User>>
|
||||||
isEmailUsed: Scalars['Boolean']
|
isEmailUsed: Scalars['Boolean']
|
||||||
myCandidates: Array<Maybe<Shout>>
|
|
||||||
reactionsByAuthor: Array<Maybe<Reaction>>
|
reactionsByAuthor: Array<Maybe<Reaction>>
|
||||||
reactionsByShout: Array<Maybe<Reaction>>
|
reactionsByShout: Array<Maybe<Reaction>>
|
||||||
reactionsForShouts: Array<Maybe<Reaction>>
|
reactionsForShouts: Array<Maybe<Reaction>>
|
||||||
recentAll: Array<Maybe<Shout>>
|
recentAll: Array<Maybe<Shout>>
|
||||||
|
recentCandidates: Array<Maybe<Shout>>
|
||||||
recentCommented: Array<Maybe<Shout>>
|
recentCommented: Array<Maybe<Shout>>
|
||||||
recentPublished: Array<Maybe<Shout>>
|
recentPublished: Array<Maybe<Shout>>
|
||||||
recentReacted: Array<Maybe<Shout>>
|
recentReacted: Array<Maybe<Shout>>
|
||||||
|
@ -397,11 +397,6 @@ export type QueryIsEmailUsedArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryMyCandidatesArgs = {
|
|
||||||
limit: Scalars['Int']
|
|
||||||
offset: Scalars['Int']
|
|
||||||
}
|
|
||||||
|
|
||||||
export type QueryReactionsByAuthorArgs = {
|
export type QueryReactionsByAuthorArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
|
@ -425,6 +420,11 @@ export type QueryRecentAllArgs = {
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryRecentCandidatesArgs = {
|
||||||
|
limit: Scalars['Int']
|
||||||
|
offset: Scalars['Int']
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryRecentCommentedArgs = {
|
export type QueryRecentCommentedArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"Feedback": "Обратная связь",
|
"Feedback": "Обратная связь",
|
||||||
"Follow": "Подписаться",
|
"Follow": "Подписаться",
|
||||||
"Follow the topic": "Подписаться на тему",
|
"Follow the topic": "Подписаться на тему",
|
||||||
"Forget password?": "Забыли пароль?",
|
"Forgot password?": "Забыли пароль?",
|
||||||
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
|
"Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале",
|
||||||
"Help to edit": "Помочь редактировать",
|
"Help to edit": "Помочь редактировать",
|
||||||
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
|
"Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики",
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"Join the community": "Присоединиться к сообществу",
|
"Join the community": "Присоединиться к сообществу",
|
||||||
"Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!",
|
"Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!",
|
||||||
"Knowledge base": "База знаний",
|
"Knowledge base": "База знаний",
|
||||||
"Load more": "Загрузить ещё",
|
"Load more": "Показать ещё",
|
||||||
"Loading": "Загрузка",
|
"Loading": "Загрузка",
|
||||||
"Manifest": "Манифест",
|
"Manifest": "Манифест",
|
||||||
"More": "Ещё",
|
"More": "Ещё",
|
||||||
|
@ -141,5 +141,7 @@
|
||||||
"topics": "темы",
|
"topics": "темы",
|
||||||
"user already exist": "пользователь уже существует",
|
"user already exist": "пользователь уже существует",
|
||||||
"view": "просмотр",
|
"view": "просмотр",
|
||||||
"zine": "журнал"
|
"zine": "журнал",
|
||||||
|
"Please, confirm email": "Пожалуйста, подтвердите электронную почту",
|
||||||
|
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,27 +3,28 @@ import type { AuthResult } from '../graphql/types.gen'
|
||||||
import { getLogger } from '../utils/logger'
|
import { getLogger } from '../utils/logger'
|
||||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
|
||||||
const log = getLogger('auth-store')
|
const log = getLogger('auth-store')
|
||||||
|
|
||||||
export const session = atom<AuthResult>()
|
const [session, setSession] = createSignal<AuthResult | null>(null)
|
||||||
|
|
||||||
export const signIn = async (params) => {
|
export const signIn = async (params) => {
|
||||||
const s = await apiClient.authLogin(params)
|
const authResult = await apiClient.authLogin(params)
|
||||||
session.set(s)
|
setSession(authResult)
|
||||||
setToken(s.token)
|
setToken(authResult.token)
|
||||||
log.debug('signed in')
|
log.debug('signed in')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signUp = async (params) => {
|
export const signUp = async (params) => {
|
||||||
const s = await apiClient.authRegiser(params)
|
const authResult = await apiClient.authRegister(params)
|
||||||
session.set(s)
|
setSession(authResult)
|
||||||
setToken(s.token)
|
setToken(authResult.token)
|
||||||
log.debug('signed up')
|
log.debug('signed up')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const signOut = () => {
|
export const signOut = () => {
|
||||||
session.set(null)
|
setSession(null)
|
||||||
resetToken()
|
resetToken()
|
||||||
log.debug('signed out')
|
log.debug('signed out')
|
||||||
}
|
}
|
||||||
|
@ -36,6 +37,18 @@ export const signCheck = async (params) => {
|
||||||
|
|
||||||
export const resetCode = atom<string>()
|
export const resetCode = atom<string>()
|
||||||
|
|
||||||
|
export const register = async ({ email, password }: { email: string; password: string }) => {
|
||||||
|
const authResult = await apiClient.authRegister({
|
||||||
|
email,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
|
||||||
|
if (authResult && !authResult.error) {
|
||||||
|
log.debug('register session update', authResult)
|
||||||
|
setSession(authResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const signSendLink = async (params) => {
|
export const signSendLink = async (params) => {
|
||||||
await apiClient.authSendLink(params) // { email }
|
await apiClient.authSendLink(params) // { email }
|
||||||
resetToken()
|
resetToken()
|
||||||
|
@ -44,11 +57,15 @@ export const signSendLink = async (params) => {
|
||||||
export const signConfirm = async (params) => {
|
export const signConfirm = async (params) => {
|
||||||
const auth = await apiClient.authConfirmCode(params) // { code }
|
const auth = await apiClient.authConfirmCode(params) // { code }
|
||||||
setToken(auth.token)
|
setToken(auth.token)
|
||||||
session.set(auth)
|
setSession(auth)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const renewSession = async () => {
|
export const renewSession = async () => {
|
||||||
const s = await apiClient.getSession() // token in header
|
const authResult = await apiClient.getSession() // token in header
|
||||||
setToken(s.token)
|
setToken(authResult.token)
|
||||||
session.set(s)
|
setSession(authResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuthStore = () => {
|
||||||
|
return { session }
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,10 @@ export const handleClientRouteLinkClick = (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
// TODO: search params
|
// TODO: search params
|
||||||
routerStore.open(url.pathname)
|
routerStore.open(url.pathname)
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import { atom, computed, map, ReadableAtom } from 'nanostores'
|
|
||||||
import type { Author, Shout, Topic } from '../../graphql/types.gen'
|
import type { Author, Shout, Topic } from '../../graphql/types.gen'
|
||||||
import type { WritableAtom } from 'nanostores'
|
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
import { addAuthorsByTopic } from './authors'
|
import { addAuthorsByTopic } from './authors'
|
||||||
import { addTopicsByAuthor } from './topics'
|
import { addTopicsByAuthor } from './topics'
|
||||||
|
@ -9,79 +6,65 @@ import { byStat } from '../../utils/sortby'
|
||||||
|
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
|
|
||||||
const log = getLogger('articles store')
|
const log = getLogger('articles store')
|
||||||
|
|
||||||
let articleEntitiesStore: WritableAtom<{ [articleSlug: string]: Shout }>
|
const [sortedArticles, setSortedArticles] = createSignal<Shout[]>([])
|
||||||
let articlesByAuthorsStore: ReadableAtom<{ [authorSlug: string]: Shout[] }>
|
const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
|
||||||
let articlesByLayoutStore: ReadableAtom<{ [layout: string]: Shout[] }>
|
|
||||||
let articlesByTopicsStore: ReadableAtom<{ [topicSlug: string]: Shout[] }>
|
|
||||||
let topViewedArticlesStore: ReadableAtom<Shout[]>
|
|
||||||
let topCommentedArticlesStore: ReadableAtom<Shout[]>
|
|
||||||
|
|
||||||
const [getSortedArticles, setSortedArticles] = createSignal<Shout[]>([])
|
const [topArticles, setTopArticles] = createSignal<Shout[]>([])
|
||||||
|
const [topMonthArticles, setTopMonthArticles] = createSignal<Shout[]>([])
|
||||||
|
|
||||||
const topArticlesStore = atom<Shout[]>()
|
const articlesByAuthor = createLazyMemo(() => {
|
||||||
const topMonthArticlesStore = atom<Shout[]>()
|
return Object.values(articleEntities()).reduce((acc, article) => {
|
||||||
|
article.authors.forEach((author) => {
|
||||||
const initStore = (initial?: Record<string, Shout>) => {
|
if (!acc[author.slug]) {
|
||||||
log.debug('initStore')
|
acc[author.slug] = []
|
||||||
if (articleEntitiesStore) {
|
|
||||||
throw new Error('articles store already initialized')
|
|
||||||
}
|
|
||||||
|
|
||||||
articleEntitiesStore = map(initial)
|
|
||||||
|
|
||||||
articlesByAuthorsStore = computed(articleEntitiesStore, (articleEntities) => {
|
|
||||||
return Object.values(articleEntities).reduce((acc, article) => {
|
|
||||||
article.authors.forEach((author) => {
|
|
||||||
if (!acc[author.slug]) {
|
|
||||||
acc[author.slug] = []
|
|
||||||
}
|
|
||||||
acc[author.slug].push(article)
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {} as { [authorSlug: string]: Shout[] })
|
|
||||||
})
|
|
||||||
|
|
||||||
articlesByTopicsStore = computed(articleEntitiesStore, (articleEntities) => {
|
|
||||||
return Object.values(articleEntities).reduce((acc, article) => {
|
|
||||||
article.topics.forEach((topic) => {
|
|
||||||
if (!acc[topic.slug]) {
|
|
||||||
acc[topic.slug] = []
|
|
||||||
}
|
|
||||||
acc[topic.slug].push(article)
|
|
||||||
})
|
|
||||||
|
|
||||||
return acc
|
|
||||||
}, {} as { [authorSlug: string]: Shout[] })
|
|
||||||
})
|
|
||||||
|
|
||||||
articlesByLayoutStore = computed(articleEntitiesStore, (articleEntities) => {
|
|
||||||
return Object.values(articleEntities).reduce((acc, article) => {
|
|
||||||
if (!acc[article.layout]) {
|
|
||||||
acc[article.layout] = []
|
|
||||||
}
|
}
|
||||||
|
acc[author.slug].push(article)
|
||||||
|
})
|
||||||
|
|
||||||
acc[article.layout].push(article)
|
return acc
|
||||||
|
}, {} as { [authorSlug: string]: Shout[] })
|
||||||
|
})
|
||||||
|
|
||||||
return acc
|
const articlesByTopic = createLazyMemo(() => {
|
||||||
}, {} as { [layout: string]: Shout[] })
|
return Object.values(articleEntities()).reduce((acc, article) => {
|
||||||
})
|
article.topics.forEach((topic) => {
|
||||||
|
if (!acc[topic.slug]) {
|
||||||
|
acc[topic.slug] = []
|
||||||
|
}
|
||||||
|
acc[topic.slug].push(article)
|
||||||
|
})
|
||||||
|
|
||||||
topViewedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
|
return acc
|
||||||
const sortedArticles = Object.values(articleEntities)
|
}, {} as { [authorSlug: string]: Shout[] })
|
||||||
sortedArticles.sort(byStat('viewed'))
|
})
|
||||||
return sortedArticles
|
|
||||||
})
|
|
||||||
|
|
||||||
topCommentedArticlesStore = computed(articleEntitiesStore, (articleEntities) => {
|
const articlesByLayout = createLazyMemo(() => {
|
||||||
const sortedArticles = Object.values(articleEntities)
|
return Object.values(articleEntities()).reduce((acc, article) => {
|
||||||
sortedArticles.sort(byStat('commented'))
|
if (!acc[article.layout]) {
|
||||||
return sortedArticles
|
acc[article.layout] = []
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
acc[article.layout].push(article)
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, {} as { [layout: string]: Shout[] })
|
||||||
|
})
|
||||||
|
|
||||||
|
const topViewedArticles = createLazyMemo(() => {
|
||||||
|
const result = Object.values(articleEntities())
|
||||||
|
result.sort(byStat('viewed'))
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
const topCommentedArticles = createLazyMemo(() => {
|
||||||
|
const result = Object.values(articleEntities())
|
||||||
|
result.sort(byStat('commented'))
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
const addArticles = (...args: Shout[][]) => {
|
const addArticles = (...args: Shout[][]) => {
|
||||||
|
@ -92,14 +75,12 @@ const addArticles = (...args: Shout[][]) => {
|
||||||
return acc
|
return acc
|
||||||
}, {} as { [articleSLug: string]: Shout })
|
}, {} as { [articleSLug: string]: Shout })
|
||||||
|
|
||||||
if (!articleEntitiesStore) {
|
setArticleEntities((prevArticleEntities) => {
|
||||||
initStore(newArticleEntities)
|
return {
|
||||||
} else {
|
...prevArticleEntities,
|
||||||
articleEntitiesStore.set({
|
|
||||||
...articleEntitiesStore.get(),
|
|
||||||
...newArticleEntities
|
...newArticleEntities
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const authorsByTopic = allArticles.reduce((acc, article) => {
|
const authorsByTopic = allArticles.reduce((acc, article) => {
|
||||||
const { authors, topics } = article
|
const { authors, topics } = article
|
||||||
|
@ -173,13 +154,13 @@ export const loadPublishedArticles = async ({
|
||||||
export const loadTopMonthArticles = async (): Promise<void> => {
|
export const loadTopMonthArticles = async (): Promise<void> => {
|
||||||
const articles = await apiClient.getTopMonthArticles()
|
const articles = await apiClient.getTopMonthArticles()
|
||||||
addArticles(articles)
|
addArticles(articles)
|
||||||
topMonthArticlesStore.set(articles)
|
setTopMonthArticles(articles)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadTopArticles = async (): Promise<void> => {
|
export const loadTopArticles = async (): Promise<void> => {
|
||||||
const articles = await apiClient.getTopArticles()
|
const articles = await apiClient.getTopArticles()
|
||||||
addArticles(articles)
|
addArticles(articles)
|
||||||
topArticlesStore.set(articles)
|
setTopArticles(articles)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadSearchResults = async ({
|
export const loadSearchResults = async ({
|
||||||
|
@ -217,32 +198,21 @@ type InitialState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useArticlesStore = (initialState: InitialState = {}) => {
|
export const useArticlesStore = (initialState: InitialState = {}) => {
|
||||||
addArticles(initialState.sortedArticles || [])
|
addArticles([...(initialState.sortedArticles || [])])
|
||||||
|
|
||||||
if (initialState.sortedArticles) {
|
if (initialState.sortedArticles) {
|
||||||
setSortedArticles([...initialState.sortedArticles])
|
setSortedArticles([...initialState.sortedArticles])
|
||||||
}
|
}
|
||||||
|
|
||||||
const getArticleEntities = useStore(articleEntitiesStore)
|
|
||||||
const getTopArticles = useStore(topArticlesStore)
|
|
||||||
const getTopMonthArticles = useStore(topMonthArticlesStore)
|
|
||||||
const getArticlesByAuthor = useStore(articlesByAuthorsStore)
|
|
||||||
const getArticlesByTopic = useStore(articlesByTopicsStore)
|
|
||||||
const getArticlesByLayout = useStore(articlesByLayoutStore)
|
|
||||||
// TODO: get from server
|
|
||||||
const getTopViewedArticles = useStore(topViewedArticlesStore)
|
|
||||||
// TODO: get from server
|
|
||||||
const getTopCommentedArticles = useStore(topCommentedArticlesStore)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getArticleEntities,
|
articleEntities,
|
||||||
getSortedArticles,
|
sortedArticles,
|
||||||
getArticlesByTopic,
|
articlesByTopic,
|
||||||
getArticlesByAuthor,
|
articlesByAuthor,
|
||||||
getTopArticles,
|
topArticles,
|
||||||
getTopMonthArticles,
|
topMonthArticles,
|
||||||
getTopViewedArticles,
|
topViewedArticles,
|
||||||
getTopCommentedArticles,
|
topCommentedArticles,
|
||||||
getArticlesByLayout
|
articlesByLayout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,36 @@
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
import type { ReadableAtom, WritableAtom } from 'nanostores'
|
|
||||||
import { atom, computed } from 'nanostores'
|
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { byCreated } from '../../utils/sortby'
|
import { byCreated } from '../../utils/sortby'
|
||||||
|
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
|
import { createSignal } from 'solid-js'
|
||||||
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
|
|
||||||
const log = getLogger('authors store')
|
const log = getLogger('authors store')
|
||||||
|
|
||||||
export type AuthorsSortBy = 'created' | 'name'
|
export type AuthorsSortBy = 'created' | 'name'
|
||||||
|
|
||||||
const sortAllByStore = atom<AuthorsSortBy>('created')
|
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('created')
|
||||||
|
|
||||||
let authorEntitiesStore: WritableAtom<{ [authorSlug: string]: Author }>
|
const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({})
|
||||||
let authorsByTopicStore: WritableAtom<{ [topicSlug: string]: Author[] }>
|
const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({})
|
||||||
let sortedAuthorsStore: ReadableAtom<Author[]>
|
|
||||||
|
|
||||||
const initStore = (initial: { [authorSlug: string]: Author }) => {
|
const sortedAuthors = createLazyMemo(() => {
|
||||||
if (authorEntitiesStore) {
|
const authors = Object.values(authorEntities())
|
||||||
return
|
switch (sortAllBy()) {
|
||||||
}
|
case 'created': {
|
||||||
|
// log.debug('sorted by created')
|
||||||
authorEntitiesStore = atom(initial)
|
authors.sort(byCreated)
|
||||||
|
break
|
||||||
sortedAuthorsStore = computed([authorEntitiesStore, sortAllByStore], (authorEntities, sortBy) => {
|
|
||||||
const authors = Object.values(authorEntities)
|
|
||||||
switch (sortBy) {
|
|
||||||
case 'created': {
|
|
||||||
// log.debug('sorted by created')
|
|
||||||
authors.sort(byCreated)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'name': {
|
|
||||||
// log.debug('sorted by name')
|
|
||||||
authors.sort((a, b) => a.name.localeCompare(b.name))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return authors
|
case 'name': {
|
||||||
})
|
// log.debug('sorted by name')
|
||||||
}
|
authors.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
break
|
||||||
export const setSortAllBy = (sortBy: AuthorsSortBy) => {
|
}
|
||||||
sortAllByStore.set(sortBy)
|
}
|
||||||
}
|
return authors
|
||||||
|
})
|
||||||
|
|
||||||
const addAuthors = (authors: Author[]) => {
|
const addAuthors = (authors: Author[]) => {
|
||||||
const newAuthorEntities = authors.reduce((acc, author) => {
|
const newAuthorEntities = authors.reduce((acc, author) => {
|
||||||
|
@ -52,24 +38,20 @@ const addAuthors = (authors: Author[]) => {
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, Author>)
|
}, {} as Record<string, Author>)
|
||||||
|
|
||||||
if (!authorEntitiesStore) {
|
setAuthorEntities((prevAuthorEntities) => {
|
||||||
initStore(newAuthorEntities)
|
return {
|
||||||
} else {
|
...prevAuthorEntities,
|
||||||
authorEntitiesStore.set({
|
|
||||||
...authorEntitiesStore.get(),
|
|
||||||
...newAuthorEntities
|
...newAuthorEntities
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[] }) => {
|
export const addAuthorsByTopic = (newAuthorsByTopic: { [topicSlug: string]: Author[] }) => {
|
||||||
const allAuthors = Object.values(authorsByTopic).flat()
|
const allAuthors = Object.values(newAuthorsByTopic).flat()
|
||||||
addAuthors(allAuthors)
|
addAuthors(allAuthors)
|
||||||
|
|
||||||
if (!authorsByTopicStore) {
|
setAuthorsByTopic((prevAuthorsByTopic) => {
|
||||||
authorsByTopicStore = atom<{ [topicSlug: string]: Author[] }>(authorsByTopic)
|
return Object.entries(newAuthorsByTopic).reduce((acc, [topicSlug, authors]) => {
|
||||||
} else {
|
|
||||||
const newState = Object.entries(authorsByTopic).reduce((acc, [topicSlug, authors]) => {
|
|
||||||
if (!acc[topicSlug]) {
|
if (!acc[topicSlug]) {
|
||||||
acc[topicSlug] = []
|
acc[topicSlug] = []
|
||||||
}
|
}
|
||||||
|
@ -81,10 +63,8 @@ export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[
|
||||||
})
|
})
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, authorsByTopicStore.get())
|
}, prevAuthorsByTopic)
|
||||||
|
})
|
||||||
authorsByTopicStore.set(newState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAllAuthors = async (): Promise<void> => {
|
export const loadAllAuthors = async (): Promise<void> => {
|
||||||
|
@ -97,12 +77,7 @@ type InitialState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthorsStore = (initialState: InitialState = {}) => {
|
export const useAuthorsStore = (initialState: InitialState = {}) => {
|
||||||
const authors = [...(initialState.authors || [])]
|
addAuthors([...(initialState.authors || [])])
|
||||||
addAuthors(authors)
|
|
||||||
|
|
||||||
const getAuthorEntities = useStore(authorEntitiesStore)
|
return { authorEntities, sortedAuthors, authorsByTopic }
|
||||||
const getSortedAuthors = useStore(sortedAuthorsStore)
|
|
||||||
const getAuthorsByTopic = useStore(authorsByTopicStore)
|
|
||||||
|
|
||||||
return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,8 +40,8 @@ export const loadReactions = async ({
|
||||||
limit: number
|
limit: number
|
||||||
offset: number
|
offset: number
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
const reactions = await apiClient.getReactionsForShouts({ shoutSlugs, limit, offset })
|
const reactionsForShouts = await apiClient.getReactionsForShouts({ shoutSlugs, limit, offset })
|
||||||
reactionsOrdered.set(reactions)
|
reactionsOrdered.set(reactionsForShouts)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createReaction = async (reaction: Reaction) =>
|
export const createReaction = async (reaction: Reaction) =>
|
||||||
|
|
|
@ -5,20 +5,17 @@ import { useAuthorsStore } from './authors'
|
||||||
const TOP_AUTHORS_COUNT = 5
|
const TOP_AUTHORS_COUNT = 5
|
||||||
|
|
||||||
export const useTopAuthorsStore = () => {
|
export const useTopAuthorsStore = () => {
|
||||||
const { getArticlesByAuthor } = useArticlesStore()
|
const { articlesByAuthor } = useArticlesStore()
|
||||||
const { getAuthorEntities } = useAuthorsStore()
|
const { authorEntities } = useAuthorsStore()
|
||||||
|
|
||||||
const getTopAuthors = createMemo(() => {
|
const topAuthors = createMemo(() => {
|
||||||
const articlesByAuthor = getArticlesByAuthor()
|
return Object.keys(articlesByAuthor())
|
||||||
const authorEntities = getAuthorEntities()
|
|
||||||
|
|
||||||
return Object.keys(articlesByAuthor)
|
|
||||||
.sort((authorSlug1, authorSlug2) => {
|
.sort((authorSlug1, authorSlug2) => {
|
||||||
const author1Rating = articlesByAuthor[authorSlug1].reduce(
|
const author1Rating = articlesByAuthor()[authorSlug1].reduce(
|
||||||
(acc, article) => acc + article.stat?.rating,
|
(acc, article) => acc + article.stat?.rating,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
const author2Rating = articlesByAuthor[authorSlug2].reduce(
|
const author2Rating = articlesByAuthor()[authorSlug2].reduce(
|
||||||
(acc, article) => acc + article.stat?.rating,
|
(acc, article) => acc + article.stat?.rating,
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
|
@ -29,9 +26,9 @@ export const useTopAuthorsStore = () => {
|
||||||
return author1Rating > author2Rating ? 1 : -1
|
return author1Rating > author2Rating ? 1 : -1
|
||||||
})
|
})
|
||||||
.slice(0, TOP_AUTHORS_COUNT)
|
.slice(0, TOP_AUTHORS_COUNT)
|
||||||
.map((authorSlug) => authorEntities[authorSlug])
|
.map((authorSlug) => authorEntities()[authorSlug])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
return { getTopAuthors }
|
return { topAuthors }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,66 +1,53 @@
|
||||||
|
import { createMemo, createSignal } from 'solid-js'
|
||||||
import { apiClient } from '../../utils/apiClient'
|
import { apiClient } from '../../utils/apiClient'
|
||||||
import { map, MapStore, ReadableAtom, atom, computed } from 'nanostores'
|
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { useStore } from '@nanostores/solid'
|
|
||||||
import { byCreated, byTopicStatDesc } from '../../utils/sortby'
|
import { byCreated, byTopicStatDesc } from '../../utils/sortby'
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
import { createSignal } from 'solid-js'
|
import { createLazyMemo } from '@solid-primitives/memo'
|
||||||
|
|
||||||
const log = getLogger('topics store')
|
const log = getLogger('topics store')
|
||||||
|
|
||||||
export type TopicsSortBy = 'created' | 'title' | 'authors' | 'shouts'
|
export type TopicsSortBy = 'created' | 'title' | 'authors' | 'shouts'
|
||||||
|
|
||||||
const sortAllByStore = atom<TopicsSortBy>('shouts')
|
const [sortAllBy, setSortAllBy] = createSignal<TopicsSortBy>('shouts')
|
||||||
|
|
||||||
let topicEntitiesStore: MapStore<Record<string, Topic>>
|
export { setSortAllBy }
|
||||||
let sortedTopicsStore: ReadableAtom<Topic[]>
|
|
||||||
let topTopicsStore: ReadableAtom<Topic[]>
|
|
||||||
|
|
||||||
const [getRandomTopics, setRandomTopics] = createSignal<Topic[]>()
|
const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({})
|
||||||
let topicsByAuthorStore: MapStore<Record<string, Topic[]>>
|
const [randomTopics, setRandomTopics] = createSignal<Topic[]>([])
|
||||||
|
const [topicsByAuthor, setTopicByAuthor] = createSignal<{ [authorSlug: string]: Topic[] }>({})
|
||||||
|
|
||||||
const initStore = (initial?: { [topicSlug: string]: Topic }) => {
|
const sortedTopics = createLazyMemo<Topic[]>(() => {
|
||||||
if (topicEntitiesStore) {
|
const topics = Object.values(topicEntities())
|
||||||
return
|
const sortAllByValue = sortAllBy()
|
||||||
}
|
|
||||||
|
|
||||||
topicEntitiesStore = map<Record<string, Topic>>(initial)
|
switch (sortAllByValue) {
|
||||||
|
case 'created': {
|
||||||
sortedTopicsStore = computed([topicEntitiesStore, sortAllByStore], (topicEntities, sortBy) => {
|
// log.debug('sorted by created')
|
||||||
const topics = Object.values(topicEntities)
|
topics.sort(byCreated)
|
||||||
switch (sortBy) {
|
break
|
||||||
case 'created': {
|
|
||||||
// log.debug('sorted by created')
|
|
||||||
topics.sort(byCreated)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'shouts':
|
|
||||||
case 'authors':
|
|
||||||
// log.debug(`sorted by ${sortBy}`)
|
|
||||||
topics.sort(byTopicStatDesc(sortBy))
|
|
||||||
break
|
|
||||||
case 'title':
|
|
||||||
// log.debug('sorted by title')
|
|
||||||
topics.sort((a, b) => a.title.localeCompare(b.title))
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
log.error(`Unknown sort: ${sortBy}`)
|
|
||||||
}
|
}
|
||||||
return topics
|
case 'shouts':
|
||||||
})
|
case 'authors':
|
||||||
|
// log.debug(`sorted by ${sortBy}`)
|
||||||
topTopicsStore = computed(topicEntitiesStore, (topicEntities) => {
|
topics.sort(byTopicStatDesc(sortAllByValue))
|
||||||
const topics = Object.values(topicEntities)
|
break
|
||||||
topics.sort(byTopicStatDesc('shouts'))
|
case 'title':
|
||||||
return topics
|
// log.debug('sorted by title')
|
||||||
})
|
topics.sort((a, b) => a.title.localeCompare(b.title))
|
||||||
}
|
break
|
||||||
|
default:
|
||||||
export const setSortAllTopicsBy = (sortBy: TopicsSortBy) => {
|
log.error(`Unknown sort: ${sortAllByValue}`)
|
||||||
if (sortAllByStore.get() !== sortBy) {
|
|
||||||
sortAllByStore.set(sortBy)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return topics
|
||||||
|
})
|
||||||
|
|
||||||
|
const topTopics = createMemo(() => {
|
||||||
|
const topics = Object.values(topicEntities())
|
||||||
|
topics.sort(byTopicStatDesc('shouts'))
|
||||||
|
return topics
|
||||||
|
})
|
||||||
|
|
||||||
const addTopics = (...args: Topic[][]) => {
|
const addTopics = (...args: Topic[][]) => {
|
||||||
const allTopics = args.flatMap((topics) => topics || [])
|
const allTopics = args.flatMap((topics) => topics || [])
|
||||||
|
@ -70,24 +57,20 @@ const addTopics = (...args: Topic[][]) => {
|
||||||
return acc
|
return acc
|
||||||
}, {} as Record<string, Topic>)
|
}, {} as Record<string, Topic>)
|
||||||
|
|
||||||
if (!topicEntitiesStore) {
|
setTopicEntities((prevTopicEntities) => {
|
||||||
initStore(newTopicEntities)
|
return {
|
||||||
} else {
|
...prevTopicEntities,
|
||||||
topicEntitiesStore.set({
|
|
||||||
...topicEntitiesStore.get(),
|
|
||||||
...newTopicEntities
|
...newTopicEntities
|
||||||
})
|
}
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic[] }) => {
|
export const addTopicsByAuthor = (newTopicsByAuthors: { [authorSlug: string]: Topic[] }) => {
|
||||||
const allTopics = Object.values(topicsByAuthors).flat()
|
const allTopics = Object.values(newTopicsByAuthors).flat()
|
||||||
addTopics(allTopics)
|
addTopics(allTopics)
|
||||||
|
|
||||||
if (!topicsByAuthorStore) {
|
setTopicByAuthor((prevTopicsByAuthor) => {
|
||||||
topicsByAuthorStore = map<Record<string, Topic[]>>(topicsByAuthors)
|
return Object.entries(newTopicsByAuthors).reduce((acc, [authorSlug, topics]) => {
|
||||||
} else {
|
|
||||||
const newState = Object.entries(topicsByAuthors).reduce((acc, [authorSlug, topics]) => {
|
|
||||||
if (!acc[authorSlug]) {
|
if (!acc[authorSlug]) {
|
||||||
acc[authorSlug] = []
|
acc[authorSlug] = []
|
||||||
}
|
}
|
||||||
|
@ -99,10 +82,8 @@ export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic
|
||||||
})
|
})
|
||||||
|
|
||||||
return acc
|
return acc
|
||||||
}, topicsByAuthorStore.get())
|
}, prevTopicsByAuthor)
|
||||||
|
})
|
||||||
topicsByAuthorStore.set(newState)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAllTopics = async (): Promise<void> => {
|
export const loadAllTopics = async (): Promise<void> => {
|
||||||
|
@ -117,24 +98,15 @@ type InitialState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTopicsStore = (initialState: InitialState = {}) => {
|
export const useTopicsStore = (initialState: InitialState = {}) => {
|
||||||
const topics = [...(initialState.topics || [])]
|
|
||||||
const randomTopics = [...(initialState.randomTopics || [])]
|
|
||||||
|
|
||||||
if (initialState.sortBy) {
|
if (initialState.sortBy) {
|
||||||
sortAllByStore.set(initialState.sortBy)
|
setSortAllBy(initialState.sortBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
addTopics(topics, randomTopics)
|
addTopics(initialState.topics, initialState.randomTopics)
|
||||||
|
|
||||||
if (randomTopics) {
|
if (initialState.randomTopics) {
|
||||||
setRandomTopics(randomTopics)
|
setRandomTopics(initialState.randomTopics)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTopicEntities = useStore(topicEntitiesStore)
|
return { topicEntities, sortedTopics, randomTopics, topTopics, topicsByAuthor }
|
||||||
|
|
||||||
const getSortedTopics = useStore(sortedTopicsStore)
|
|
||||||
|
|
||||||
const getTopTopics = useStore(topTopicsStore)
|
|
||||||
|
|
||||||
return { getTopicEntities, getSortedTopics, getRandomTopics, getTopTopics }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,3 @@
|
||||||
header {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-logo {
|
.main-logo {
|
||||||
height: 80px !important;
|
height: 80px !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
overscroll-behavior-y: none;
|
overscroll-behavior-y: none;
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Reaction, Shout, FollowingEntity } from '../graphql/types.gen'
|
import type { Reaction, Shout, FollowingEntity, AuthResult } from '../graphql/types.gen'
|
||||||
|
|
||||||
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||||
import articleBySlug from '../graphql/query/article-by-slug'
|
import articleBySlug from '../graphql/query/article-by-slug'
|
||||||
|
@ -36,15 +36,43 @@ const log = getLogger('api-client')
|
||||||
const FEED_SIZE = 50
|
const FEED_SIZE = 50
|
||||||
const REACTIONS_PAGE_SIZE = 100
|
const REACTIONS_PAGE_SIZE = 100
|
||||||
|
|
||||||
export const apiClient = {
|
type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found'
|
||||||
// auth
|
|
||||||
|
|
||||||
authLogin: async ({ email, password }) => {
|
export class ApiError extends Error {
|
||||||
|
code: ApiErrorCode
|
||||||
|
|
||||||
|
constructor(code: ApiErrorCode, message?: string) {
|
||||||
|
super(message)
|
||||||
|
this.code = code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const apiClient = {
|
||||||
|
authLogin: async ({ email, password }): Promise<AuthResult> => {
|
||||||
const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
|
const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
|
||||||
|
// log.debug('authLogin', { response })
|
||||||
|
if (response.error) {
|
||||||
|
if (response.error.message === '[GraphQL] User not found') {
|
||||||
|
throw new ApiError('user_not_found')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError('unknown', response.error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data.signIn.error) {
|
||||||
|
if (response.data.signIn.error === 'please, confirm email') {
|
||||||
|
throw new ApiError('email_not_confirmed')
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApiError('unknown', response.data.signIn.error)
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.signIn
|
return response.data.signIn
|
||||||
},
|
},
|
||||||
authRegiser: async ({ email, password }) => {
|
authRegister: async ({ email, password }): Promise<AuthResult> => {
|
||||||
const response = await publicGraphQLClient.query(authRegisterMutation, { email, password }).toPromise()
|
const response = await publicGraphQLClient
|
||||||
|
.mutation(authRegisterMutation, { email, password })
|
||||||
|
.toPromise()
|
||||||
return response.data.registerUser
|
return response.data.registerUser
|
||||||
},
|
},
|
||||||
authSignOut: async () => {
|
authSignOut: async () => {
|
||||||
|
@ -87,9 +115,12 @@ export const apiClient = {
|
||||||
return response.data.recentPublished
|
return response.data.recentPublished
|
||||||
},
|
},
|
||||||
getRandomTopics: async ({ amount }: { amount: number }) => {
|
getRandomTopics: async ({ amount }: { amount: number }) => {
|
||||||
log.debug('getRandomTopics')
|
|
||||||
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
|
const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise()
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
log.error('getRandomTopics', response.error)
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.topicsRandom
|
return response.data.topicsRandom
|
||||||
},
|
},
|
||||||
getSearchResults: async ({
|
getSearchResults: async ({
|
||||||
|
@ -177,7 +208,7 @@ export const apiClient = {
|
||||||
return response.data.unfollow
|
return response.data.unfollow
|
||||||
},
|
},
|
||||||
|
|
||||||
getSession: async () => {
|
getSession: async (): Promise<AuthResult> => {
|
||||||
// renew session with auth token in header (!)
|
// renew session with auth token in header (!)
|
||||||
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
||||||
return response.data.refreshSession
|
return response.data.refreshSession
|
||||||
|
|
16
src/utils/scroll.ts
Normal file
16
src/utils/scroll.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
const scrollPosition = {
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export const saveScrollPosition = () => {
|
||||||
|
scrollPosition.top = window.scrollY
|
||||||
|
scrollPosition.left = window.scrollX
|
||||||
|
}
|
||||||
|
|
||||||
|
export const restoreScrollPosition = () => {
|
||||||
|
window.scroll({
|
||||||
|
top: scrollPosition.top,
|
||||||
|
left: scrollPosition.left
|
||||||
|
})
|
||||||
|
}
|
13
yarn.lock
13
yarn.lock
|
@ -2733,6 +2733,14 @@
|
||||||
"@solid-primitives/rootless" "^1.1.3"
|
"@solid-primitives/rootless" "^1.1.3"
|
||||||
"@solid-primitives/utils" "^3.0.2"
|
"@solid-primitives/utils" "^3.0.2"
|
||||||
|
|
||||||
|
"@solid-primitives/memo@^1.0.2":
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@solid-primitives/memo/-/memo-1.0.2.tgz#7a33216e665a94ac85413be206dacf3f295221d0"
|
||||||
|
integrity sha512-I4BKJAItiRxjR1ngc+gWsdpiz3V79LQdgxxRlFPp3K+8Oi2dolXweDlLKKX5qec8cSuhV99gTfsxEoVBMkzNgQ==
|
||||||
|
dependencies:
|
||||||
|
"@solid-primitives/scheduled" "1.0.1"
|
||||||
|
"@solid-primitives/utils" "^3.0.2"
|
||||||
|
|
||||||
"@solid-primitives/platform@^0.0.101":
|
"@solid-primitives/platform@^0.0.101":
|
||||||
version "0.0.101"
|
version "0.0.101"
|
||||||
resolved "https://registry.yarnpkg.com/@solid-primitives/platform/-/platform-0.0.101.tgz#7bfa879152a59169589e2dc999aac8ceb63233c7"
|
resolved "https://registry.yarnpkg.com/@solid-primitives/platform/-/platform-0.0.101.tgz#7bfa879152a59169589e2dc999aac8ceb63233c7"
|
||||||
|
@ -2763,6 +2771,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@solid-primitives/utils" "^3.0.2"
|
"@solid-primitives/utils" "^3.0.2"
|
||||||
|
|
||||||
|
"@solid-primitives/scheduled@1.0.1":
|
||||||
|
version "1.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@solid-primitives/scheduled/-/scheduled-1.0.1.tgz#e5b07452f39d27504c4ba1caa64d65020110c017"
|
||||||
|
integrity sha512-zRyW9L4nYdL0yZktvJz/Ye9kVNa6UW26L71sZEqzzHnxvDidbT+mln4np7jqFrAeGiWMwWnRDR/ZvM0FK85jMw==
|
||||||
|
|
||||||
"@solid-primitives/scheduled@^1.0.1":
|
"@solid-primitives/scheduled@^1.0.1":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@solid-primitives/scheduled/-/scheduled-1.0.2.tgz#8c2e8511b9b361c22c13e78377dc4168cc9c0452"
|
resolved "https://registry.yarnpkg.com/@solid-primitives/scheduled/-/scheduled-1.0.2.tgz#8c2e8511b9b361c22c13e78377dc4168cc9c0452"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user