diff --git a/.eslintrc.js b/.eslintrc.js index c0284d22..13d10867 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,10 +34,8 @@ module.exports = { varsIgnorePattern: '^log$' } ], - // TODO: Remove any usage and enable - '@typescript-eslint/no-explicit-any': 'off', - // TODO: Fix errors and enable this rule - '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', // solid-js fix 'import/no-unresolved': [2, { ignore: ['solid-js/'] }] @@ -58,8 +56,8 @@ module.exports = { // FIXME 'solid/reactivity': 'off', - // TODO: Should be enabled - 'promise/catch-or-return': 'off', + // Should be enabled + // 'promise/catch-or-return': 'off', 'solid/no-innerhtml': 'off', @@ -77,7 +75,8 @@ module.exports = { eqeqeq: 'error', 'no-param-reassign': 'error', - 'no-nested-ternary': 'error' + 'no-nested-ternary': 'error', + 'no-shadow': 'error' }, settings: { 'import/resolver': { diff --git a/package.json b/package.json index 45607736..c1707089 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@nanostores/persistent": "^0.7.0", "@nanostores/router": "^0.7.0", "@nanostores/solid": "^0.3.0", + "@solid-primitives/memo": "^1.0.2", "axios": "^0.27.2", "google-translate-api-x": "^10.4.1", "loglevel": "^1.8.0", diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index 317acc88..094d957e 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -7,8 +7,7 @@ import { createEffect, createMemo, createSignal, For, onMount, Show } from 'soli import type { Author, Reaction, Shout } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { showModal } from '../../stores/ui' -import { useStore } from '@nanostores/solid' -import { session } from '../../stores/auth' +import { useAuthStore } from '../../stores/auth' import { incrementView } from '../../stores/zine/articles' import { renderMarkdown } from '@astrojs/markdown-remark' import { markdownOptions } from '../../../mdx.config' @@ -41,7 +40,7 @@ const formatDate = (date: Date) => { export const FullArticle = (props: ArticleProps) => { const [body, setBody] = createSignal(props.article.body?.startsWith('<') ? props.article.body : '') - const auth = useStore(session) + const { session } = useAuthStore() createEffect(() => { if (body() || !props.article.body) { @@ -51,7 +50,9 @@ export const FullArticle = (props: ArticleProps) => { if (props.article.body.startsWith('<')) { setBody(props.article.body) } 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) => { )} - + {t('To leave a comment you please')} { - + diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx index 177bcf3d..5e9c5451 100644 --- a/src/components/Author/Card.tsx +++ b/src/components/Author/Card.tsx @@ -6,10 +6,9 @@ import './Card.scss' import { createMemo } from 'solid-js' import { translit } from '../../utils/ru2en' import { t } from '../../utils/intl' -import { session } from '../../stores/auth' +import { useAuthStore } from '../../stores/auth' import { locale } from '../../stores/ui' import { follow, unfollow } from '../../stores/zine/common' -import { useStore } from '@nanostores/solid' interface AuthorCardProps { compact?: boolean @@ -21,14 +20,15 @@ interface AuthorCardProps { } export const AuthorCard = (props: AuthorCardProps) => { - const auth = useStore(session) + const { session } = useAuthStore() + const subscribed = createMemo( () => - !!auth() + !!session() ?.info?.authors?.filter((u) => u === props.author.slug) .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 name = () => { return props.author.name === 'Дискурс' && locale() !== 'ru' diff --git a/src/components/Feed/Beside.tsx b/src/components/Feed/Beside.tsx index bfa3a8c3..e4f894f8 100644 --- a/src/components/Feed/Beside.tsx +++ b/src/components/Feed/Beside.tsx @@ -10,7 +10,7 @@ import { Icon } from '../Nav/Icon' import { t } from '../../utils/intl' interface BesideProps { - title: string + title?: string values: any[] beside: Shout wrapper: 'topic' | 'author' | 'article' | 'top-article' diff --git a/src/components/Feed/Sidebar.tsx b/src/components/Feed/Sidebar.tsx index 09cfc0ca..3eadd9ef 100644 --- a/src/components/Feed/Sidebar.tsx +++ b/src/components/Feed/Sidebar.tsx @@ -1,7 +1,6 @@ -import { useStore } from '@nanostores/solid' import { For } from 'solid-js' import type { Author } from '../../graphql/types.gen' -import { session } from '../../stores/auth' +import { useAuthStore } from '../../stores/auth' import { useAuthorsStore } from '../../stores/zine/authors' import { t } from '../../utils/intl' import { Icon } from '../Nav/Icon' @@ -15,13 +14,13 @@ type FeedSidebarProps = { export const FeedSidebar = (props: FeedSidebarProps) => { const { getSeen: seen } = useSeenStore() - const auth = useStore(session) - const { getSortedAuthors: authors } = useAuthorsStore({ authors: props.authors }) - const { getArticlesByTopic } = useArticlesStore() - const { getTopicEntities } = useTopicsStore() + const { session } = useAuthStore() + const { authorEntities } = useAuthorsStore({ authors: props.authors }) + const { articlesByTopic } = useArticlesStore() + const { topicEntities } = useTopicsStore() 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) => { @@ -64,22 +63,22 @@ export const FeedSidebar = (props: FeedSidebarProps) => { - + {(authorSlug) => ( @{authorSlug} - {authors()[authorSlug].name} + {authorEntities()[authorSlug].name} )} - + {(topicSlug) => ( - {getTopicEntities()[topicSlug]?.title} + {topicEntities()[topicSlug]?.title} )} diff --git a/src/components/Layouts/MainLayout.tsx b/src/components/Layouts/MainLayout.tsx index 355e5a19..7d9aa638 100644 --- a/src/components/Layouts/MainLayout.tsx +++ b/src/components/Layouts/MainLayout.tsx @@ -3,21 +3,23 @@ import { Header } from '../Nav/Header' import { Footer } from '../Discours/Footer' import '../../styles/app.scss' +import { Show } from 'solid-js' -type Props = { +type MainLayoutProps = { headerTitle?: string children: JSX.Element isHeaderFixed?: boolean + hideFooter?: boolean } -export const MainLayout = (props: Props) => { - const isHeaderFixed = props.isHeaderFixed !== undefined ? props.isHeaderFixed : true - +export const MainLayout = (props: MainLayoutProps) => { return ( <> - + {props.children} - + + + > ) } diff --git a/src/components/Nav/AuthModal.tsx b/src/components/Nav/AuthModal.tsx index d64e2043..9f1729ba 100644 --- a/src/components/Nav/AuthModal.tsx +++ b/src/components/Nav/AuthModal.tsx @@ -5,11 +5,10 @@ import './AuthModal.scss' import { Form } from 'solid-js-form' import { t } from '../../utils/intl' import { hideModal, useModalStore } from '../../stores/ui' -import { useStore } from '@nanostores/solid' -import { session as sessionstore, signIn } from '../../stores/auth' -import { apiClient } from '../../utils/apiClient' +import { useAuthStore, signIn, register } from '../../stores/auth' import { useValidator } from '../../utils/validators' import { baseUrl } from '../../graphql/publicGraphQLClient' +import { ApiError } from '../../utils/apiClient' type AuthMode = 'sign-in' | 'sign-up' | 'forget' | 'reset' | 'resend' | 'password' @@ -23,7 +22,7 @@ const statuses: { [key: string]: string } = { const titles = { 'sign-up': t('Create account'), 'sign-in': t('Enter the Discours'), - forget: t('Forget password?'), + forget: t('Forgot password?'), reset: t('Please, confirm your email to finish'), resend: t('Resend code'), password: t('Enter your new password') @@ -34,10 +33,10 @@ const titles = { // FIXME !!! // eslint-disable-next-line sonarjs/cognitive-complexity export default (props: { code?: string; mode?: string }) => { - const session = useStore(sessionstore) + const { session } = useAuthStore() const [handshaking] = createSignal(false) const { getModal } = useModalStore() - const [authError, setError] = createSignal('') + const [authError, setError] = createSignal('') const [mode, setMode] = createSignal('sign-in') const [validation, setValidation] = createSignal({}) const [initial, setInitial] = createSignal({}) @@ -46,7 +45,7 @@ export default (props: { code?: string; mode?: string }) => { let passElement: HTMLInputElement | undefined let codeElement: HTMLInputElement | undefined - // 3rd party providier auth handler + // 3rd party provider auth handler const oauth = (provider: string): void => { const popup = window.open(`${baseUrl}/oauth/${provider}`, provider, 'width=740, height=420') popup?.focus() @@ -85,28 +84,50 @@ export default (props: { code?: string; mode?: string }) => { setValidation(vs) setInitial(ini) } + onMount(setupValidators) + const resetError = () => { + setError('') + } + + const changeMode = (newMode: AuthMode) => { + setMode(newMode) + resetError() + } + // local auth handler const localAuth = async () => { console.log('[auth] native account processing') switch (mode()) { 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 case 'sign-up': if (pass2Element?.value !== passElement?.value) { setError(t('Passwords are not equal')) } else { - // FIXME use store actions - const r = await apiClient.authRegiser({ + await register({ email: emailElement?.value, password: passElement?.value }) - if (r) { - console.debug('[auth] session update', r) - sessionstore.set(r) - } } break case 'reset': @@ -130,6 +151,7 @@ export default (props: { code?: string; mode?: string }) => { } } + // FIXME move to handlers createEffect(() => { if (session()?.user?.slug && getModal() === 'auth') { // hiding itself if finished @@ -141,7 +163,8 @@ export default (props: { code?: string; mode?: string }) => { } else { console.log('[auth] session', session()) } - }, [session()]) + }) + return ( @@ -174,7 +197,6 @@ export default (props: { code?: string; mode?: string }) => { > {titles[mode()]} - { {t('Everything is ok, please give us your email address')} - @@ -195,7 +216,6 @@ export default (props: { code?: string; mode?: string }) => { - {/*FIXME*/} {/**/} {/* */} @@ -223,7 +243,6 @@ export default (props: { code?: string; mode?: string }) => { {t('Email')} - { {t('Reset code')} - { {handshaking() ? '...' : titles[mode()]} - - setMode('forget')}> - {t('Forget password?')} + { + ev.preventDefault() + changeMode('forget') + }} + > + {t('Forgot password?')} - {t('Or continue with social network')} @@ -297,25 +319,24 @@ export default (props: { code?: string; mode?: string }) => { - - setMode('sign-in')}> + changeMode('sign-in')}> {t('I have an account')} - setMode('sign-up')}> + changeMode('sign-up')}> {t('I have no account yet')} - setMode('sign-in')}> + changeMode('sign-in')}> {t('I know the password')} - setMode('resend')}> + changeMode('resend')}> {t('Resend code')} diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index 2e4728f5..7ff2b622 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -6,8 +6,7 @@ import { Modal } from './Modal' import AuthModal from './AuthModal' import { t } from '../../utils/intl' import { useModalStore, showModal, useWarningsStore } from '../../stores/ui' -import { useStore } from '@nanostores/solid' -import { session as ssession } from '../../stores/auth' +import { useAuthStore } from '../../stores/auth' import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router' import './Header.scss' import { getPagePath } from '@nanostores/router' @@ -38,7 +37,7 @@ export const Header = (props: Props) => { const [visibleWarnings, setVisibleWarnings] = createSignal(false) // stores const { getWarnings } = useWarningsStore() - const session = useStore(ssession) + const { session } = useAuthStore() const { getModal } = useModalStore() const { getPage } = useRouter() diff --git a/src/components/Nav/Private.tsx b/src/components/Nav/Private.tsx index 3b4ec787..4675a614 100644 --- a/src/components/Nav/Private.tsx +++ b/src/components/Nav/Private.tsx @@ -2,12 +2,11 @@ import type { Author } from '../../graphql/types.gen' import Userpic from '../Author/Userpic' import { Icon } from './Icon' import './Private.scss' -import { session as sesstore } from '../../stores/auth' -import { useStore } from '@nanostores/solid' +import { useAuthStore } from '../../stores/auth' import { useRouter } from '../../stores/router' export default () => { - const session = useStore(sesstore) + const { session } = useAuthStore() const { getPage } = useRouter() return ( diff --git a/src/components/Nav/ProfileModal.tsx b/src/components/Nav/ProfileModal.tsx index 1f873a86..38d07060 100644 --- a/src/components/Nav/ProfileModal.tsx +++ b/src/components/Nav/ProfileModal.tsx @@ -3,8 +3,7 @@ import { AuthorCard } from '../Author/Card' import type { Author } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { hideModal } from '../../stores/ui' -import { session, signOut } from '../../stores/auth' -import { useStore } from '@nanostores/solid' +import { useAuthStore, signOut } from '../../stores/auth' import { createMemo } from 'solid-js' const quit = () => { @@ -13,29 +12,32 @@ const quit = () => { } export default () => { - const auth = useStore(session) - const author = createMemo(() => { - const a = { + const { session } = useAuthStore() + + const author = createMemo(() => { + const a: Author = { name: 'anonymous', userpic: '', slug: '' - } as Author - if (auth()?.user?.slug) { - const u = auth().user + } + + if (session()?.user?.slug) { + const u = session().user a.name = u.name a.slug = u.slug a.userpic = u.userpic } + return a }) // TODO: ProfileModal markup and styles return ( - {auth()?.user?.username} + {session()?.user?.username} - {auth()?.user?.bio || ''} - {(l: string) => {l}} + {session()?.user?.bio || ''} + {(l: string) => {l}} {t('Quit')} ) diff --git a/src/components/Nav/Topics.scss b/src/components/Nav/Topics.scss index e5decc94..a0fe24b6 100644 --- a/src/components/Nav/Topics.scss +++ b/src/components/Nav/Topics.scss @@ -42,7 +42,6 @@ } a:hover { - font-weight: 500; color: white; } } diff --git a/src/components/Nav/Topics.tsx b/src/components/Nav/Topics.tsx index 93ede4d7..14750cf9 100644 --- a/src/components/Nav/Topics.tsx +++ b/src/components/Nav/Topics.tsx @@ -6,7 +6,8 @@ import { t } from '../../utils/intl' import { locale } from '../../stores/ui' 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 return ( @@ -14,10 +15,10 @@ export const NavTopics = (props: { topics: Topic[] }) => { 0}> - {(t: Topic) => ( + {(topic) => ( - - #{tag(t)} + + #{tag(topic)} )} diff --git a/src/components/Pages/ArticlePage.tsx b/src/components/Pages/ArticlePage.tsx index 773665c6..74c8a338 100644 --- a/src/components/Pages/ArticlePage.tsx +++ b/src/components/Pages/ArticlePage.tsx @@ -18,17 +18,17 @@ export const ArticlePage = (props: PageProps) => { throw new Error('ts guard') } - const { getArticleEntities } = useArticlesStore({ + const { articleEntities } = useArticlesStore({ sortedArticles }) - const article = createMemo(() => getArticleEntities()[page.params.slug]) + const article = createMemo(() => articleEntities()[page.params.slug]) onMount(() => { const slug = page.params.slug - const article = getArticleEntities()[slug] + const articleValue = articleEntities()[slug] - if (!article || !article.body) { + if (!articleValue || !articleValue.body) { loadArticle({ slug }) } }) diff --git a/src/components/Pages/FourOuFourPage.tsx b/src/components/Pages/FourOuFourPage.tsx index 38d955cf..68d5cb97 100644 --- a/src/components/Pages/FourOuFourPage.tsx +++ b/src/components/Pages/FourOuFourPage.tsx @@ -3,7 +3,7 @@ import { MainLayout } from '../Layouts/MainLayout' export const FourOuFourPage = () => { return ( - + ) diff --git a/src/components/Root.tsx b/src/components/Root.tsx index bd1c619c..7e65cbee 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -2,43 +2,49 @@ // import 'solid-devtools' 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 { Dynamic, isServer } from 'solid-js/web' import { getLogger } from '../utils/logger' import type { PageProps } from './types' -// do not remove -// for debugging, to disable lazy loading -// import HomePage from './Pages/HomePage' -// import AllTopicsPage from './Pages/AllTopicsPage' -// import TopicPage from './Pages/TopicPage' -// import AllAuthorsPage from './Pages/AllAuthorsPage' -// import AuthorPage from './Pages/AuthorPage' -// import FeedPage from './Pages/FeedPage' -// import ArticlePage from './Pages/ArticlePage' -// import SearchPage from './Pages/SearchPage' -// import FourOuFourPage from './Pages/FourOuFourPage' +import { HomePage } from './Pages/HomePage' +import { AllTopicsPage } from './Pages/AllTopicsPage' +import { TopicPage } from './Pages/TopicPage' +import { AllAuthorsPage } from './Pages/AllAuthorsPage' +import { AuthorPage } from './Pages/AuthorPage' +import { FeedPage } from './Pages/FeedPage' +import { ArticlePage } from './Pages/ArticlePage' +import { SearchPage } from './Pages/SearchPage' +import { FourOuFourPage } from './Pages/FourOuFourPage' +import { DogmaPage } from './Pages/about/DogmaPage' +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')) -const AllTopicsPage = lazy(() => import('./Pages/AllTopicsPage')) -const TopicPage = lazy(() => import('./Pages/TopicPage')) -const AllAuthorsPage = lazy(() => import('./Pages/AllAuthorsPage')) -const AuthorPage = lazy(() => import('./Pages/AuthorPage')) -const FeedPage = lazy(() => import('./Pages/FeedPage')) -const ArticlePage = lazy(() => import('./Pages/ArticlePage')) -const SearchPage = lazy(() => import('./Pages/SearchPage')) -const FourOuFourPage = lazy(() => import('./Pages/FourOuFourPage')) -const DogmaPage = lazy(() => import('./Pages/about/DogmaPage')) - -const GuidePage = lazy(() => import('./Pages/about/GuidePage')) -const HelpPage = lazy(() => import('./Pages/about/HelpPage')) -const ManifestPage = lazy(() => import('./Pages/about/ManifestPage')) -const PartnersPage = lazy(() => import('./Pages/about/PartnersPage')) -const ProjectsPage = lazy(() => import('./Pages/about/ProjectsPage')) -const TermsOfUsePage = lazy(() => import('./Pages/about/TermsOfUsePage')) -const ThanksPage = lazy(() => import('./Pages/about/ThanksPage')) +// TODO: lazy load +// const HomePage = lazy(() => import('./Pages/HomePage')) +// const AllTopicsPage = lazy(() => import('./Pages/AllTopicsPage')) +// const TopicPage = lazy(() => import('./Pages/TopicPage')) +// const AllAuthorsPage = lazy(() => import('./Pages/AllAuthorsPage')) +// const AuthorPage = lazy(() => import('./Pages/AuthorPage')) +// const FeedPage = lazy(() => import('./Pages/FeedPage')) +// const ArticlePage = lazy(() => import('./Pages/ArticlePage')) +// const SearchPage = lazy(() => import('./Pages/SearchPage')) +// const FourOuFourPage = lazy(() => import('./Pages/FourOuFourPage')) +// const DogmaPage = lazy(() => import('./Pages/about/DogmaPage')) +// const GuidePage = lazy(() => import('./Pages/about/GuidePage')) +// const HelpPage = lazy(() => import('./Pages/about/HelpPage')) +// const ManifestPage = lazy(() => import('./Pages/about/ManifestPage')) +// const PartnersPage = lazy(() => import('./Pages/about/PartnersPage')) +// const ProjectsPage = lazy(() => import('./Pages/about/ProjectsPage')) +// const TermsOfUsePage = lazy(() => import('./Pages/about/TermsOfUsePage')) +// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage')) const log = getLogger('root') diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index 815fd3b4..2544ae6c 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -6,9 +6,11 @@ import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' -import { useStore } from '@nanostores/solid' -import { session } from '../../stores/auth' +import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' +import { getLogger } from '../../utils/logger' + +const log = getLogger('TopicCard') interface TopicProps { topic: Topic @@ -19,63 +21,73 @@ interface TopicProps { } export const TopicCard = (props: TopicProps) => { - const auth = useStore(session) - const topic = createMemo(() => props.topic) + const { session } = useAuthStore() + 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 const subscribe = async (really = true) => { if (really) { - follow({ what: FollowingEntity.Topic, slug: topic().slug }) + follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) } else { - unfollow({ what: FollowingEntity.Topic, slug: topic().slug }) + unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }) } } return ( - + - {capitalize(topic().title || '')} + {capitalize(props.topic.title || '')} - + - - + + - + - {topic().body} + {props.topic.body} - + - {topic().stat?.shouts + + {props.topic.stat?.shouts + ' ' + t('post') + - plural(topic().stat?.shouts || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])} + plural( + props.topic.stat?.shouts || 0, + locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'] + )} - {topic().stat?.authors + + {props.topic.stat?.authors + ' ' + t('author') + - plural(topic().stat?.authors || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'])} + plural( + props.topic.stat?.authors || 0, + locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'] + )} - {topic().stat?.followers + + {props.topic.stat?.followers + ' ' + t('follower') + plural( - topic().stat?.followers || 0, + props.topic.stat?.followers || 0, locale() === 'ru' ? ['ов', '', 'а'] : ['s', '', 's'] )} diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index d62216a4..a7641366 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -3,8 +3,7 @@ import { Show } from 'solid-js/web' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import './Full.scss' -import { session } from '../../stores/auth' -import { useStore } from '@nanostores/solid' +import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' @@ -13,8 +12,9 @@ type Props = { } export const FullTopic = (props: Props) => { - const auth = useStore(session) - const subscribed = createMemo(() => auth()?.info?.topics?.includes(props.topic?.slug)) + const { session } = useAuthStore() + + const subscribed = createMemo(() => session()?.info?.topics?.includes(props.topic?.slug)) return ( diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index 38da7bf0..2c141739 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -7,8 +7,7 @@ import { Icon } from '../Nav/Icon' import { t } from '../../utils/intl' import { useAuthorsStore } from '../../stores/zine/authors' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' -import { session } from '../../stores/auth' -import { useStore } from '@nanostores/solid' +import { useAuthStore } from '../../stores/auth' import '../../styles/AllTopics.scss' type AllAuthorsPageSearchParams = { @@ -20,19 +19,21 @@ type Props = { } export const AllAuthorsView = (props: Props) => { - const { getSortedAuthors: authorslist } = useAuthorsStore({ authors: props.authors }) + const { sortedAuthors: authorList } = useAuthorsStore({ authors: props.authors }) const [sortedAuthors, setSortedAuthors] = createSignal([]) const [sortedKeys, setSortedKeys] = 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() createEffect(() => { if ((!getSearchParams().by || getSearchParams().by === 'name') && abc().length === 0) { console.log('[authors] default grouping by abc') - const grouped = { ...groupByName(authorslist()) } + const grouped = { ...groupByName(authorList()) } grouped['A-Z'] = sortBy(grouped['A-Z'], byFirstChar) setAbc(grouped) const keys = Object.keys(abc) @@ -40,13 +41,13 @@ export const AllAuthorsView = (props: Props) => { setSortedKeys(keys as string[]) } else { console.log('[authors] sorting by ' + getSearchParams().by) - setSortedAuthors(sortBy(authorslist(), getSearchParams().by)) + setSortedAuthors(sortBy(authorList(), getSearchParams().by)) } - }, [authorslist(), getSearchParams().by]) + }, [authorList(), getSearchParams().by]) return ( - + 0}> diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 786e1822..857fb1eb 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -2,39 +2,42 @@ import { createEffect, For, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { Icon } from '../Nav/Icon' 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 { TopicCard } from '../Topic/Card' -import { session } from '../../stores/auth' -import { useStore } from '@nanostores/solid' +import { useAuthStore } from '../../stores/auth' import '../../styles/AllTopics.scss' +import { getLogger } from '../../utils/logger' + +const log = getLogger('AllTopicsView') type AllTopicsPageSearchParams = { by: 'shouts' | 'authors' | 'title' | '' } -type Props = { +type AllTopicsViewProps = { topics: Topic[] } -export const AllTopicsView = (props: Props) => { +export const AllTopicsView = (props: AllTopicsViewProps) => { const { getSearchParams, changeSearchParam } = useRouter() - const { getSortedTopics } = useTopicsStore({ + const { sortedTopics } = useTopicsStore({ topics: props.topics, sortBy: getSearchParams().by || 'shouts' }) - const auth = useStore(session) + + const { session } = useAuthStore() createEffect(() => { 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 ( - 0}> + 0}> @@ -78,7 +81,7 @@ export const AllTopicsView = (props: Props) => { - + {(topic) => ( )} diff --git a/src/components/Views/Author.tsx b/src/components/Views/Author.tsx index 32a0b7d0..16f43e3e 100644 --- a/src/components/Views/Author.tsx +++ b/src/components/Views/Author.tsx @@ -25,12 +25,12 @@ type AuthorPageSearchParams = { } export const AuthorView = (props: AuthorProps) => { - const { getSortedArticles: articles } = useArticlesStore({ + const { sortedArticles } = useArticlesStore({ 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() //const slug = createMemo(() => author().slug) @@ -90,7 +90,7 @@ export const AuthorView = (props: AuthorProps) => { {title()} - 0}> + 0}> {/*FIXME*/} {/* { {/* wrapper={'topic'}*/} {/* topicShortDescription={true}*/} {/*/>*/} - - - - + + + + diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index bfa79ffb..756124cc 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -7,9 +7,8 @@ import { TopicCard } from '../Topic/Card' import { ArticleCard } from '../Feed/Card' import { AuthorCard } from '../Author/Card' import { t } from '../../utils/intl' -import { useStore } from '@nanostores/solid' import { FeedSidebar } from '../Feed/Sidebar' -import { session } from '../../stores/auth' +import { useAuthStore } from '../../stores/auth' import CommentCard from '../Article/Comment' import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles' import { useReactionsStore } from '../../stores/zine/reactions' @@ -31,13 +30,12 @@ interface FeedProps { export const FeedView = (props: FeedProps) => { // state - const { getSortedArticles: articles } = useArticlesStore({ sortedArticles: props.articles }) + const { sortedArticles } = useArticlesStore({ sortedArticles: props.articles }) const reactions = useReactionsStore() - const { getSortedAuthors: authors } = useAuthorsStore() - const { getTopTopics } = useTopicsStore() - const { getTopAuthors } = useTopAuthorsStore() - - const auth = useStore(session) + const { sortedAuthors } = useAuthorsStore() + const { topTopics } = useTopicsStore() + const { topAuthors } = useTopAuthorsStore() + const { session } = useAuthStore() const topReactions = createMemo(() => sortBy(reactions(), byCreated)) @@ -66,12 +64,12 @@ export const FeedView = (props: FeedProps) => { - + - + {t('My feed')} @@ -87,8 +85,8 @@ export const FeedView = (props: FeedProps) => { - 0}> - + 0}> + {(article) => } @@ -101,7 +99,7 @@ export const FeedView = (props: FeedProps) => { - + {(author) => ( @@ -110,7 +108,7 @@ export const FeedView = (props: FeedProps) => { - + {(article) => } @@ -127,10 +125,10 @@ export const FeedView = (props: FeedProps) => { {(comment) => } - 0}> + 0}> {t('Topics')} - + {(topic) => } diff --git a/src/components/Views/FourOuFour.tsx b/src/components/Views/FourOuFour.tsx index 706bc38f..40c8e540 100644 --- a/src/components/Views/FourOuFour.tsx +++ b/src/components/Views/FourOuFour.tsx @@ -1,7 +1,7 @@ import { t } from '../../utils/intl' import { Icon } from '../Nav/Icon' import styles from '../../styles/FourOuFour.module.scss' -import clsx from 'clsx' +import { clsx } from 'clsx' export const FourOuFourView = (_props) => { return ( diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index a2eb8854..edeaccf1 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -23,6 +23,7 @@ import { } from '../../stores/zine/articles' import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { locale } from '../../stores/ui' +import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' const log = getLogger('home view') @@ -30,50 +31,51 @@ type HomeProps = { randomTopics: Topic[] recentPublishedArticles: Shout[] } - -const CLIENT_LOAD_ARTICLES_COUNT = 30 -const LOAD_MORE_ARTICLES_COUNT = 30 +const PRERENDERED_ARTICLES_COUNT = 5 +const CLIENT_LOAD_ARTICLES_COUNT = 29 +const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3 export const HomeView = (props: HomeProps) => { const { - getSortedArticles, - getTopArticles, - getTopMonthArticles, - getTopViewedArticles, - getTopCommentedArticles, - getArticlesByLayout + sortedArticles, + topArticles, + topMonthArticles, + topViewedArticles, + topCommentedArticles, + articlesByLayout } = useArticlesStore({ sortedArticles: props.recentPublishedArticles }) - const { getRandomTopics, getTopTopics } = useTopicsStore({ + const { randomTopics, topTopics } = useTopicsStore({ randomTopics: props.randomTopics }) - const { getTopAuthors } = useTopAuthorsStore() + const { topAuthors } = useTopAuthorsStore() onMount(() => { loadTopArticles() 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 articlesByLayout = getArticlesByLayout() - const filledLayouts = Object.keys(articlesByLayout).filter( + const filledLayouts = Object.keys(articlesByLayout()).filter( // 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)] : '' return ( - + - + } /> @@ -81,69 +83,95 @@ export const HomeView = (props: HomeProps) => { ) }) - const loadMore = () => { - loadPublishedArticles({ limit: LOAD_MORE_ARTICLES_COUNT, offset: getSortedArticles().length }) + const loadMore = async () => { + saveScrollPosition() + await loadPublishedArticles({ limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length }) + restoreScrollPosition() } - return ( - - + const pages = createMemo(() => { + return sortedArticles() + .slice(PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) + .reduce((acc, article, index) => { + if (index % LOAD_MORE_PAGE_SIZE === 0) { + acc.push([]) + } - + acc[acc.length - 1].push(article) + return acc + }, [] as Shout[][]) + }) + + return ( + + + + - - - - - {/*FIXME: ?*/} - + 5}> + + + + + + + + + + + + + + {t('Top commented')}} /> + + {randomLayout()} + + + + + + + + + + + + + - - - - - - - - - {t('Top commented')}} /> - - {randomLayout()} - - - - - - - - - - - - - - - {(article) => } + + {(page) => ( + <> + + + + + + + + > + )} + diff --git a/src/components/Views/Search.tsx b/src/components/Views/Search.tsx index a66a4de2..75f45f9c 100644 --- a/src/components/Views/Search.tsx +++ b/src/components/Views/Search.tsx @@ -16,7 +16,7 @@ type 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 { getSearchParams } = useRouter() @@ -66,12 +66,12 @@ export const SearchView = (props: Props) => { - 0}> + 0}> {t('Publications')} - + {(article) => ( diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index e418efa8..0ae118ce 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -24,12 +24,12 @@ interface TopicProps { export const TopicView = (props: TopicProps) => { const { getSearchParams, changeSearchParam } = useRouter() - const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles }) - const { getTopicEntities } = useTopicsStore({ topics: [props.topic] }) + const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles }) + 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(() => { @@ -104,7 +104,7 @@ export const TopicView = (props: TopicProps) => { 5}> diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 761cee55..3f593788 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -324,11 +324,11 @@ export type Query = { getUserRoles: Array> getUsersBySlugs: Array> isEmailUsed: Scalars['Boolean'] - myCandidates: Array> reactionsByAuthor: Array> reactionsByShout: Array> reactionsForShouts: Array> recentAll: Array> + recentCandidates: Array> recentCommented: Array> recentPublished: Array> recentReacted: Array> @@ -397,11 +397,6 @@ export type QueryIsEmailUsedArgs = { email: Scalars['String'] } -export type QueryMyCandidatesArgs = { - limit: Scalars['Int'] - offset: Scalars['Int'] -} - export type QueryReactionsByAuthorArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -425,6 +420,11 @@ export type QueryRecentAllArgs = { offset: Scalars['Int'] } +export type QueryRecentCandidatesArgs = { + limit: Scalars['Int'] + offset: Scalars['Int'] +} + export type QueryRecentCommentedArgs = { limit: Scalars['Int'] offset: Scalars['Int'] diff --git a/src/locales/ru.json b/src/locales/ru.json index 719f5eac..90264e81 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -39,7 +39,7 @@ "Feedback": "Обратная связь", "Follow": "Подписаться", "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": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале", "Help to edit": "Помочь редактировать", "Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики", @@ -52,7 +52,7 @@ "Join the community": "Присоединиться к сообществу", "Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!", "Knowledge base": "База знаний", - "Load more": "Загрузить ещё", + "Load more": "Показать ещё", "Loading": "Загрузка", "Manifest": "Манифест", "More": "Ещё", @@ -141,5 +141,7 @@ "topics": "темы", "user already exist": "пользователь уже существует", "view": "просмотр", - "zine": "журнал" + "zine": "журнал", + "Please, confirm email": "Пожалуйста, подтвердите электронную почту", + "Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль" } diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 95d9e68b..5cc13fad 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -3,27 +3,28 @@ import type { AuthResult } from '../graphql/types.gen' import { getLogger } from '../utils/logger' import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { apiClient } from '../utils/apiClient' +import { createSignal } from 'solid-js' const log = getLogger('auth-store') -export const session = atom() +const [session, setSession] = createSignal(null) export const signIn = async (params) => { - const s = await apiClient.authLogin(params) - session.set(s) - setToken(s.token) + const authResult = await apiClient.authLogin(params) + setSession(authResult) + setToken(authResult.token) log.debug('signed in') } export const signUp = async (params) => { - const s = await apiClient.authRegiser(params) - session.set(s) - setToken(s.token) + const authResult = await apiClient.authRegister(params) + setSession(authResult) + setToken(authResult.token) log.debug('signed up') } export const signOut = () => { - session.set(null) + setSession(null) resetToken() log.debug('signed out') } @@ -36,6 +37,18 @@ export const signCheck = async (params) => { export const resetCode = atom() +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) => { await apiClient.authSendLink(params) // { email } resetToken() @@ -44,11 +57,15 @@ export const signSendLink = async (params) => { export const signConfirm = async (params) => { const auth = await apiClient.authConfirmCode(params) // { code } setToken(auth.token) - session.set(auth) + setSession(auth) } export const renewSession = async () => { - const s = await apiClient.getSession() // token in header - setToken(s.token) - session.set(s) + const authResult = await apiClient.getSession() // token in header + setToken(authResult.token) + setSession(authResult) +} + +export const useAuthStore = () => { + return { session } } diff --git a/src/stores/router.ts b/src/stores/router.ts index 4cf374d0..d0d7d51f 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -69,6 +69,10 @@ export const handleClientRouteLinkClick = (event) => { event.preventDefault() // TODO: search params routerStore.open(url.pathname) + window.scrollTo({ + top: 0, + left: 0 + }) } } } diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index eb283f5f..f3310868 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -1,7 +1,4 @@ -import { atom, computed, map, ReadableAtom } from 'nanostores' 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 { addAuthorsByTopic } from './authors' import { addTopicsByAuthor } from './topics' @@ -9,79 +6,65 @@ import { byStat } from '../../utils/sortby' import { getLogger } from '../../utils/logger' import { createSignal } from 'solid-js' +import { createLazyMemo } from '@solid-primitives/memo' const log = getLogger('articles store') -let articleEntitiesStore: WritableAtom<{ [articleSlug: string]: Shout }> -let articlesByAuthorsStore: ReadableAtom<{ [authorSlug: string]: Shout[] }> -let articlesByLayoutStore: ReadableAtom<{ [layout: string]: Shout[] }> -let articlesByTopicsStore: ReadableAtom<{ [topicSlug: string]: Shout[] }> -let topViewedArticlesStore: ReadableAtom -let topCommentedArticlesStore: ReadableAtom +const [sortedArticles, setSortedArticles] = createSignal([]) +const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({}) -const [getSortedArticles, setSortedArticles] = createSignal([]) +const [topArticles, setTopArticles] = createSignal([]) +const [topMonthArticles, setTopMonthArticles] = createSignal([]) -const topArticlesStore = atom() -const topMonthArticlesStore = atom() - -const initStore = (initial?: Record) => { - log.debug('initStore') - 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] = [] +const articlesByAuthor = createLazyMemo(() => { + return Object.values(articleEntities()).reduce((acc, article) => { + article.authors.forEach((author) => { + if (!acc[author.slug]) { + acc[author.slug] = [] } + acc[author.slug].push(article) + }) - acc[article.layout].push(article) + return acc + }, {} as { [authorSlug: string]: Shout[] }) +}) - return acc - }, {} as { [layout: string]: Shout[] }) - }) +const articlesByTopic = createLazyMemo(() => { + 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) => { - const sortedArticles = Object.values(articleEntities) - sortedArticles.sort(byStat('viewed')) - return sortedArticles - }) + return acc + }, {} as { [authorSlug: string]: Shout[] }) +}) - topCommentedArticlesStore = computed(articleEntitiesStore, (articleEntities) => { - const sortedArticles = Object.values(articleEntities) - sortedArticles.sort(byStat('commented')) - return sortedArticles - }) -} +const articlesByLayout = createLazyMemo(() => { + return Object.values(articleEntities()).reduce((acc, article) => { + if (!acc[article.layout]) { + 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 const addArticles = (...args: Shout[][]) => { @@ -92,14 +75,12 @@ const addArticles = (...args: Shout[][]) => { return acc }, {} as { [articleSLug: string]: Shout }) - if (!articleEntitiesStore) { - initStore(newArticleEntities) - } else { - articleEntitiesStore.set({ - ...articleEntitiesStore.get(), + setArticleEntities((prevArticleEntities) => { + return { + ...prevArticleEntities, ...newArticleEntities - }) - } + } + }) const authorsByTopic = allArticles.reduce((acc, article) => { const { authors, topics } = article @@ -173,13 +154,13 @@ export const loadPublishedArticles = async ({ export const loadTopMonthArticles = async (): Promise => { const articles = await apiClient.getTopMonthArticles() addArticles(articles) - topMonthArticlesStore.set(articles) + setTopMonthArticles(articles) } export const loadTopArticles = async (): Promise => { const articles = await apiClient.getTopArticles() addArticles(articles) - topArticlesStore.set(articles) + setTopArticles(articles) } export const loadSearchResults = async ({ @@ -217,32 +198,21 @@ type InitialState = { } export const useArticlesStore = (initialState: InitialState = {}) => { - addArticles(initialState.sortedArticles || []) + addArticles([...(initialState.sortedArticles || [])]) if (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 { - getArticleEntities, - getSortedArticles, - getArticlesByTopic, - getArticlesByAuthor, - getTopArticles, - getTopMonthArticles, - getTopViewedArticles, - getTopCommentedArticles, - getArticlesByLayout + articleEntities, + sortedArticles, + articlesByTopic, + articlesByAuthor, + topArticles, + topMonthArticles, + topViewedArticles, + topCommentedArticles, + articlesByLayout } } diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index fc1914f7..c09604dc 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -1,50 +1,36 @@ import { apiClient } from '../../utils/apiClient' -import type { ReadableAtom, WritableAtom } from 'nanostores' -import { atom, computed } from 'nanostores' import type { Author } from '../../graphql/types.gen' -import { useStore } from '@nanostores/solid' import { byCreated } from '../../utils/sortby' import { getLogger } from '../../utils/logger' +import { createSignal } from 'solid-js' +import { createLazyMemo } from '@solid-primitives/memo' const log = getLogger('authors store') export type AuthorsSortBy = 'created' | 'name' -const sortAllByStore = atom('created') +const [sortAllBy, setSortAllBy] = createSignal('created') -let authorEntitiesStore: WritableAtom<{ [authorSlug: string]: Author }> -let authorsByTopicStore: WritableAtom<{ [topicSlug: string]: Author[] }> -let sortedAuthorsStore: ReadableAtom +const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({}) +const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({}) -const initStore = (initial: { [authorSlug: string]: Author }) => { - if (authorEntitiesStore) { - return - } - - authorEntitiesStore = atom(initial) - - 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 - } +const sortedAuthors = createLazyMemo(() => { + const authors = Object.values(authorEntities()) + switch (sortAllBy()) { + case 'created': { + // log.debug('sorted by created') + authors.sort(byCreated) + break } - return authors - }) -} - -export const setSortAllBy = (sortBy: AuthorsSortBy) => { - sortAllByStore.set(sortBy) -} + case 'name': { + // log.debug('sorted by name') + authors.sort((a, b) => a.name.localeCompare(b.name)) + break + } + } + return authors +}) const addAuthors = (authors: Author[]) => { const newAuthorEntities = authors.reduce((acc, author) => { @@ -52,24 +38,20 @@ const addAuthors = (authors: Author[]) => { return acc }, {} as Record) - if (!authorEntitiesStore) { - initStore(newAuthorEntities) - } else { - authorEntitiesStore.set({ - ...authorEntitiesStore.get(), + setAuthorEntities((prevAuthorEntities) => { + return { + ...prevAuthorEntities, ...newAuthorEntities - }) - } + } + }) } -export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[] }) => { - const allAuthors = Object.values(authorsByTopic).flat() +export const addAuthorsByTopic = (newAuthorsByTopic: { [topicSlug: string]: Author[] }) => { + const allAuthors = Object.values(newAuthorsByTopic).flat() addAuthors(allAuthors) - if (!authorsByTopicStore) { - authorsByTopicStore = atom<{ [topicSlug: string]: Author[] }>(authorsByTopic) - } else { - const newState = Object.entries(authorsByTopic).reduce((acc, [topicSlug, authors]) => { + setAuthorsByTopic((prevAuthorsByTopic) => { + return Object.entries(newAuthorsByTopic).reduce((acc, [topicSlug, authors]) => { if (!acc[topicSlug]) { acc[topicSlug] = [] } @@ -81,10 +63,8 @@ export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[ }) return acc - }, authorsByTopicStore.get()) - - authorsByTopicStore.set(newState) - } + }, prevAuthorsByTopic) + }) } export const loadAllAuthors = async (): Promise => { @@ -97,12 +77,7 @@ type InitialState = { } export const useAuthorsStore = (initialState: InitialState = {}) => { - const authors = [...(initialState.authors || [])] - addAuthors(authors) + addAuthors([...(initialState.authors || [])]) - const getAuthorEntities = useStore(authorEntitiesStore) - const getSortedAuthors = useStore(sortedAuthorsStore) - const getAuthorsByTopic = useStore(authorsByTopicStore) - - return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic } + return { authorEntities, sortedAuthors, authorsByTopic } } diff --git a/src/stores/zine/reactions.ts b/src/stores/zine/reactions.ts index 11f38204..7b5dc576 100644 --- a/src/stores/zine/reactions.ts +++ b/src/stores/zine/reactions.ts @@ -40,8 +40,8 @@ export const loadReactions = async ({ limit: number offset: number }): Promise => { - const reactions = await apiClient.getReactionsForShouts({ shoutSlugs, limit, offset }) - reactionsOrdered.set(reactions) + const reactionsForShouts = await apiClient.getReactionsForShouts({ shoutSlugs, limit, offset }) + reactionsOrdered.set(reactionsForShouts) } export const createReaction = async (reaction: Reaction) => diff --git a/src/stores/zine/topAuthors.ts b/src/stores/zine/topAuthors.ts index 16529e69..400313f8 100644 --- a/src/stores/zine/topAuthors.ts +++ b/src/stores/zine/topAuthors.ts @@ -5,20 +5,17 @@ import { useAuthorsStore } from './authors' const TOP_AUTHORS_COUNT = 5 export const useTopAuthorsStore = () => { - const { getArticlesByAuthor } = useArticlesStore() - const { getAuthorEntities } = useAuthorsStore() + const { articlesByAuthor } = useArticlesStore() + const { authorEntities } = useAuthorsStore() - const getTopAuthors = createMemo(() => { - const articlesByAuthor = getArticlesByAuthor() - const authorEntities = getAuthorEntities() - - return Object.keys(articlesByAuthor) + const topAuthors = createMemo(() => { + return Object.keys(articlesByAuthor()) .sort((authorSlug1, authorSlug2) => { - const author1Rating = articlesByAuthor[authorSlug1].reduce( + const author1Rating = articlesByAuthor()[authorSlug1].reduce( (acc, article) => acc + article.stat?.rating, 0 ) - const author2Rating = articlesByAuthor[authorSlug2].reduce( + const author2Rating = articlesByAuthor()[authorSlug2].reduce( (acc, article) => acc + article.stat?.rating, 0 ) @@ -29,9 +26,9 @@ export const useTopAuthorsStore = () => { return author1Rating > author2Rating ? 1 : -1 }) .slice(0, TOP_AUTHORS_COUNT) - .map((authorSlug) => authorEntities[authorSlug]) + .map((authorSlug) => authorEntities()[authorSlug]) .filter(Boolean) }) - return { getTopAuthors } + return { topAuthors } } diff --git a/src/stores/zine/topics.ts b/src/stores/zine/topics.ts index abcd06fb..8994375e 100644 --- a/src/stores/zine/topics.ts +++ b/src/stores/zine/topics.ts @@ -1,66 +1,53 @@ +import { createMemo, createSignal } from 'solid-js' import { apiClient } from '../../utils/apiClient' -import { map, MapStore, ReadableAtom, atom, computed } from 'nanostores' import type { Topic } from '../../graphql/types.gen' -import { useStore } from '@nanostores/solid' import { byCreated, byTopicStatDesc } from '../../utils/sortby' import { getLogger } from '../../utils/logger' -import { createSignal } from 'solid-js' +import { createLazyMemo } from '@solid-primitives/memo' const log = getLogger('topics store') export type TopicsSortBy = 'created' | 'title' | 'authors' | 'shouts' -const sortAllByStore = atom('shouts') +const [sortAllBy, setSortAllBy] = createSignal('shouts') -let topicEntitiesStore: MapStore> -let sortedTopicsStore: ReadableAtom -let topTopicsStore: ReadableAtom +export { setSortAllBy } -const [getRandomTopics, setRandomTopics] = createSignal() -let topicsByAuthorStore: MapStore> +const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({}) +const [randomTopics, setRandomTopics] = createSignal([]) +const [topicsByAuthor, setTopicByAuthor] = createSignal<{ [authorSlug: string]: Topic[] }>({}) -const initStore = (initial?: { [topicSlug: string]: Topic }) => { - if (topicEntitiesStore) { - return - } +const sortedTopics = createLazyMemo(() => { + const topics = Object.values(topicEntities()) + const sortAllByValue = sortAllBy() - topicEntitiesStore = map>(initial) - - sortedTopicsStore = computed([topicEntitiesStore, sortAllByStore], (topicEntities, sortBy) => { - const topics = Object.values(topicEntities) - switch (sortBy) { - 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}`) + switch (sortAllByValue) { + case 'created': { + // log.debug('sorted by created') + topics.sort(byCreated) + break } - return topics - }) - - topTopicsStore = computed(topicEntitiesStore, (topicEntities) => { - const topics = Object.values(topicEntities) - topics.sort(byTopicStatDesc('shouts')) - return topics - }) -} - -export const setSortAllTopicsBy = (sortBy: TopicsSortBy) => { - if (sortAllByStore.get() !== sortBy) { - sortAllByStore.set(sortBy) + case 'shouts': + case 'authors': + // log.debug(`sorted by ${sortBy}`) + topics.sort(byTopicStatDesc(sortAllByValue)) + break + case 'title': + // log.debug('sorted by title') + topics.sort((a, b) => a.title.localeCompare(b.title)) + break + default: + log.error(`Unknown sort: ${sortAllByValue}`) } -} + + return topics +}) + +const topTopics = createMemo(() => { + const topics = Object.values(topicEntities()) + topics.sort(byTopicStatDesc('shouts')) + return topics +}) const addTopics = (...args: Topic[][]) => { const allTopics = args.flatMap((topics) => topics || []) @@ -70,24 +57,20 @@ const addTopics = (...args: Topic[][]) => { return acc }, {} as Record) - if (!topicEntitiesStore) { - initStore(newTopicEntities) - } else { - topicEntitiesStore.set({ - ...topicEntitiesStore.get(), + setTopicEntities((prevTopicEntities) => { + return { + ...prevTopicEntities, ...newTopicEntities - }) - } + } + }) } -export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic[] }) => { - const allTopics = Object.values(topicsByAuthors).flat() +export const addTopicsByAuthor = (newTopicsByAuthors: { [authorSlug: string]: Topic[] }) => { + const allTopics = Object.values(newTopicsByAuthors).flat() addTopics(allTopics) - if (!topicsByAuthorStore) { - topicsByAuthorStore = map>(topicsByAuthors) - } else { - const newState = Object.entries(topicsByAuthors).reduce((acc, [authorSlug, topics]) => { + setTopicByAuthor((prevTopicsByAuthor) => { + return Object.entries(newTopicsByAuthors).reduce((acc, [authorSlug, topics]) => { if (!acc[authorSlug]) { acc[authorSlug] = [] } @@ -99,10 +82,8 @@ export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic }) return acc - }, topicsByAuthorStore.get()) - - topicsByAuthorStore.set(newState) - } + }, prevTopicsByAuthor) + }) } export const loadAllTopics = async (): Promise => { @@ -117,24 +98,15 @@ type InitialState = { } export const useTopicsStore = (initialState: InitialState = {}) => { - const topics = [...(initialState.topics || [])] - const randomTopics = [...(initialState.randomTopics || [])] - if (initialState.sortBy) { - sortAllByStore.set(initialState.sortBy) + setSortAllBy(initialState.sortBy) } - addTopics(topics, randomTopics) + addTopics(initialState.topics, initialState.randomTopics) - if (randomTopics) { - setRandomTopics(randomTopics) + if (initialState.randomTopics) { + setRandomTopics(initialState.randomTopics) } - const getTopicEntities = useStore(topicEntitiesStore) - - const getSortedTopics = useStore(sortedTopicsStore) - - const getTopTopics = useStore(topTopicsStore) - - return { getTopicEntities, getSortedTopics, getRandomTopics, getTopTopics } + return { topicEntities, sortedTopics, randomTopics, topTopics, topicsByAuthor } } diff --git a/src/styles/FourOuFour.module.scss b/src/styles/FourOuFour.module.scss index 3ae39ce7..54da05f3 100644 --- a/src/styles/FourOuFour.module.scss +++ b/src/styles/FourOuFour.module.scss @@ -1,11 +1,3 @@ -header { - position: absolute; -} - -footer { - display: none; -} - .main-logo { height: 80px !important; } diff --git a/src/styles/app.scss b/src/styles/app.scss index b78be650..ebca34a5 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -26,7 +26,6 @@ html { height: 100%; -webkit-font-smoothing: antialiased; overscroll-behavior-y: none; - scroll-behavior: smooth; } body { diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 45e605f1..54d5c7e3 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -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 articleBySlug from '../graphql/query/article-by-slug' @@ -36,15 +36,43 @@ const log = getLogger('api-client') const FEED_SIZE = 50 const REACTIONS_PAGE_SIZE = 100 -export const apiClient = { - // auth +type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' - 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 => { 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 }, - authRegiser: async ({ email, password }) => { - const response = await publicGraphQLClient.query(authRegisterMutation, { email, password }).toPromise() + authRegister: async ({ email, password }): Promise => { + const response = await publicGraphQLClient + .mutation(authRegisterMutation, { email, password }) + .toPromise() return response.data.registerUser }, authSignOut: async () => { @@ -87,9 +115,12 @@ export const apiClient = { return response.data.recentPublished }, getRandomTopics: async ({ amount }: { amount: number }) => { - log.debug('getRandomTopics') const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise() + if (!response.data) { + log.error('getRandomTopics', response.error) + } + return response.data.topicsRandom }, getSearchResults: async ({ @@ -177,7 +208,7 @@ export const apiClient = { return response.data.unfollow }, - getSession: async () => { + getSession: async (): Promise => { // renew session with auth token in header (!) const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() return response.data.refreshSession diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts new file mode 100644 index 00000000..e3f2d240 --- /dev/null +++ b/src/utils/scroll.ts @@ -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 + }) +} diff --git a/yarn.lock b/yarn.lock index adec712d..162f528d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2733,6 +2733,14 @@ "@solid-primitives/rootless" "^1.1.3" "@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": version "0.0.101" resolved "https://registry.yarnpkg.com/@solid-primitives/platform/-/platform-0.0.101.tgz#7bfa879152a59169589e2dc999aac8ceb63233c7" @@ -2763,6 +2771,11 @@ dependencies: "@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": version "1.0.2" resolved "https://registry.yarnpkg.com/@solid-primitives/scheduled/-/scheduled-1.0.2.tgz#8c2e8511b9b361c22c13e78377dc4168cc9c0452"
diff --git a/src/components/Views/Search.tsx b/src/components/Views/Search.tsx index a66a4de2..75f45f9c 100644 --- a/src/components/Views/Search.tsx +++ b/src/components/Views/Search.tsx @@ -16,7 +16,7 @@ type 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 { getSearchParams } = useRouter() @@ -66,12 +66,12 @@ export const SearchView = (props: Props) => { - 0}> + 0}> {t('Publications')} - + {(article) => ( diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index e418efa8..0ae118ce 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -24,12 +24,12 @@ interface TopicProps { export const TopicView = (props: TopicProps) => { const { getSearchParams, changeSearchParam } = useRouter() - const { getSortedArticles: sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles }) - const { getTopicEntities } = useTopicsStore({ topics: [props.topic] }) + const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles }) + 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(() => { @@ -104,7 +104,7 @@ export const TopicView = (props: TopicProps) => { 5}> diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 761cee55..3f593788 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -324,11 +324,11 @@ export type Query = { getUserRoles: Array> getUsersBySlugs: Array> isEmailUsed: Scalars['Boolean'] - myCandidates: Array> reactionsByAuthor: Array> reactionsByShout: Array> reactionsForShouts: Array> recentAll: Array> + recentCandidates: Array> recentCommented: Array> recentPublished: Array> recentReacted: Array> @@ -397,11 +397,6 @@ export type QueryIsEmailUsedArgs = { email: Scalars['String'] } -export type QueryMyCandidatesArgs = { - limit: Scalars['Int'] - offset: Scalars['Int'] -} - export type QueryReactionsByAuthorArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -425,6 +420,11 @@ export type QueryRecentAllArgs = { offset: Scalars['Int'] } +export type QueryRecentCandidatesArgs = { + limit: Scalars['Int'] + offset: Scalars['Int'] +} + export type QueryRecentCommentedArgs = { limit: Scalars['Int'] offset: Scalars['Int'] diff --git a/src/locales/ru.json b/src/locales/ru.json index 719f5eac..90264e81 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -39,7 +39,7 @@ "Feedback": "Обратная связь", "Follow": "Подписаться", "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": "Познакомитесь с выдающимися людьми нашего времени, участвуйте в редактировании и обсуждении статей, выступайте экспертом, оценивайте материалы других авторов со всего мира и определяйте, какие статьи будут опубликованы в журнале", "Help to edit": "Помочь редактировать", "Horizontal collaborative journalistic platform": "Горизонтальная платформа для коллаборативной журналистики", @@ -52,7 +52,7 @@ "Join the community": "Присоединиться к сообществу", "Join the global community of authors!": "Присоединятесь к глобальному сообществу авторов со всего мира!", "Knowledge base": "База знаний", - "Load more": "Загрузить ещё", + "Load more": "Показать ещё", "Loading": "Загрузка", "Manifest": "Манифест", "More": "Ещё", @@ -141,5 +141,7 @@ "topics": "темы", "user already exist": "пользователь уже существует", "view": "просмотр", - "zine": "журнал" + "zine": "журнал", + "Please, confirm email": "Пожалуйста, подтвердите электронную почту", + "Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль" } diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 95d9e68b..5cc13fad 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -3,27 +3,28 @@ import type { AuthResult } from '../graphql/types.gen' import { getLogger } from '../utils/logger' import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { apiClient } from '../utils/apiClient' +import { createSignal } from 'solid-js' const log = getLogger('auth-store') -export const session = atom() +const [session, setSession] = createSignal(null) export const signIn = async (params) => { - const s = await apiClient.authLogin(params) - session.set(s) - setToken(s.token) + const authResult = await apiClient.authLogin(params) + setSession(authResult) + setToken(authResult.token) log.debug('signed in') } export const signUp = async (params) => { - const s = await apiClient.authRegiser(params) - session.set(s) - setToken(s.token) + const authResult = await apiClient.authRegister(params) + setSession(authResult) + setToken(authResult.token) log.debug('signed up') } export const signOut = () => { - session.set(null) + setSession(null) resetToken() log.debug('signed out') } @@ -36,6 +37,18 @@ export const signCheck = async (params) => { export const resetCode = atom() +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) => { await apiClient.authSendLink(params) // { email } resetToken() @@ -44,11 +57,15 @@ export const signSendLink = async (params) => { export const signConfirm = async (params) => { const auth = await apiClient.authConfirmCode(params) // { code } setToken(auth.token) - session.set(auth) + setSession(auth) } export const renewSession = async () => { - const s = await apiClient.getSession() // token in header - setToken(s.token) - session.set(s) + const authResult = await apiClient.getSession() // token in header + setToken(authResult.token) + setSession(authResult) +} + +export const useAuthStore = () => { + return { session } } diff --git a/src/stores/router.ts b/src/stores/router.ts index 4cf374d0..d0d7d51f 100644 --- a/src/stores/router.ts +++ b/src/stores/router.ts @@ -69,6 +69,10 @@ export const handleClientRouteLinkClick = (event) => { event.preventDefault() // TODO: search params routerStore.open(url.pathname) + window.scrollTo({ + top: 0, + left: 0 + }) } } } diff --git a/src/stores/zine/articles.ts b/src/stores/zine/articles.ts index eb283f5f..f3310868 100644 --- a/src/stores/zine/articles.ts +++ b/src/stores/zine/articles.ts @@ -1,7 +1,4 @@ -import { atom, computed, map, ReadableAtom } from 'nanostores' 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 { addAuthorsByTopic } from './authors' import { addTopicsByAuthor } from './topics' @@ -9,79 +6,65 @@ import { byStat } from '../../utils/sortby' import { getLogger } from '../../utils/logger' import { createSignal } from 'solid-js' +import { createLazyMemo } from '@solid-primitives/memo' const log = getLogger('articles store') -let articleEntitiesStore: WritableAtom<{ [articleSlug: string]: Shout }> -let articlesByAuthorsStore: ReadableAtom<{ [authorSlug: string]: Shout[] }> -let articlesByLayoutStore: ReadableAtom<{ [layout: string]: Shout[] }> -let articlesByTopicsStore: ReadableAtom<{ [topicSlug: string]: Shout[] }> -let topViewedArticlesStore: ReadableAtom -let topCommentedArticlesStore: ReadableAtom +const [sortedArticles, setSortedArticles] = createSignal([]) +const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({}) -const [getSortedArticles, setSortedArticles] = createSignal([]) +const [topArticles, setTopArticles] = createSignal([]) +const [topMonthArticles, setTopMonthArticles] = createSignal([]) -const topArticlesStore = atom() -const topMonthArticlesStore = atom() - -const initStore = (initial?: Record) => { - log.debug('initStore') - 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] = [] +const articlesByAuthor = createLazyMemo(() => { + return Object.values(articleEntities()).reduce((acc, article) => { + article.authors.forEach((author) => { + if (!acc[author.slug]) { + acc[author.slug] = [] } + acc[author.slug].push(article) + }) - acc[article.layout].push(article) + return acc + }, {} as { [authorSlug: string]: Shout[] }) +}) - return acc - }, {} as { [layout: string]: Shout[] }) - }) +const articlesByTopic = createLazyMemo(() => { + 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) => { - const sortedArticles = Object.values(articleEntities) - sortedArticles.sort(byStat('viewed')) - return sortedArticles - }) + return acc + }, {} as { [authorSlug: string]: Shout[] }) +}) - topCommentedArticlesStore = computed(articleEntitiesStore, (articleEntities) => { - const sortedArticles = Object.values(articleEntities) - sortedArticles.sort(byStat('commented')) - return sortedArticles - }) -} +const articlesByLayout = createLazyMemo(() => { + return Object.values(articleEntities()).reduce((acc, article) => { + if (!acc[article.layout]) { + 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 const addArticles = (...args: Shout[][]) => { @@ -92,14 +75,12 @@ const addArticles = (...args: Shout[][]) => { return acc }, {} as { [articleSLug: string]: Shout }) - if (!articleEntitiesStore) { - initStore(newArticleEntities) - } else { - articleEntitiesStore.set({ - ...articleEntitiesStore.get(), + setArticleEntities((prevArticleEntities) => { + return { + ...prevArticleEntities, ...newArticleEntities - }) - } + } + }) const authorsByTopic = allArticles.reduce((acc, article) => { const { authors, topics } = article @@ -173,13 +154,13 @@ export const loadPublishedArticles = async ({ export const loadTopMonthArticles = async (): Promise => { const articles = await apiClient.getTopMonthArticles() addArticles(articles) - topMonthArticlesStore.set(articles) + setTopMonthArticles(articles) } export const loadTopArticles = async (): Promise => { const articles = await apiClient.getTopArticles() addArticles(articles) - topArticlesStore.set(articles) + setTopArticles(articles) } export const loadSearchResults = async ({ @@ -217,32 +198,21 @@ type InitialState = { } export const useArticlesStore = (initialState: InitialState = {}) => { - addArticles(initialState.sortedArticles || []) + addArticles([...(initialState.sortedArticles || [])]) if (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 { - getArticleEntities, - getSortedArticles, - getArticlesByTopic, - getArticlesByAuthor, - getTopArticles, - getTopMonthArticles, - getTopViewedArticles, - getTopCommentedArticles, - getArticlesByLayout + articleEntities, + sortedArticles, + articlesByTopic, + articlesByAuthor, + topArticles, + topMonthArticles, + topViewedArticles, + topCommentedArticles, + articlesByLayout } } diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index fc1914f7..c09604dc 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -1,50 +1,36 @@ import { apiClient } from '../../utils/apiClient' -import type { ReadableAtom, WritableAtom } from 'nanostores' -import { atom, computed } from 'nanostores' import type { Author } from '../../graphql/types.gen' -import { useStore } from '@nanostores/solid' import { byCreated } from '../../utils/sortby' import { getLogger } from '../../utils/logger' +import { createSignal } from 'solid-js' +import { createLazyMemo } from '@solid-primitives/memo' const log = getLogger('authors store') export type AuthorsSortBy = 'created' | 'name' -const sortAllByStore = atom('created') +const [sortAllBy, setSortAllBy] = createSignal('created') -let authorEntitiesStore: WritableAtom<{ [authorSlug: string]: Author }> -let authorsByTopicStore: WritableAtom<{ [topicSlug: string]: Author[] }> -let sortedAuthorsStore: ReadableAtom +const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({}) +const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({}) -const initStore = (initial: { [authorSlug: string]: Author }) => { - if (authorEntitiesStore) { - return - } - - authorEntitiesStore = atom(initial) - - 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 - } +const sortedAuthors = createLazyMemo(() => { + const authors = Object.values(authorEntities()) + switch (sortAllBy()) { + case 'created': { + // log.debug('sorted by created') + authors.sort(byCreated) + break } - return authors - }) -} - -export const setSortAllBy = (sortBy: AuthorsSortBy) => { - sortAllByStore.set(sortBy) -} + case 'name': { + // log.debug('sorted by name') + authors.sort((a, b) => a.name.localeCompare(b.name)) + break + } + } + return authors +}) const addAuthors = (authors: Author[]) => { const newAuthorEntities = authors.reduce((acc, author) => { @@ -52,24 +38,20 @@ const addAuthors = (authors: Author[]) => { return acc }, {} as Record) - if (!authorEntitiesStore) { - initStore(newAuthorEntities) - } else { - authorEntitiesStore.set({ - ...authorEntitiesStore.get(), + setAuthorEntities((prevAuthorEntities) => { + return { + ...prevAuthorEntities, ...newAuthorEntities - }) - } + } + }) } -export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[] }) => { - const allAuthors = Object.values(authorsByTopic).flat() +export const addAuthorsByTopic = (newAuthorsByTopic: { [topicSlug: string]: Author[] }) => { + const allAuthors = Object.values(newAuthorsByTopic).flat() addAuthors(allAuthors) - if (!authorsByTopicStore) { - authorsByTopicStore = atom<{ [topicSlug: string]: Author[] }>(authorsByTopic) - } else { - const newState = Object.entries(authorsByTopic).reduce((acc, [topicSlug, authors]) => { + setAuthorsByTopic((prevAuthorsByTopic) => { + return Object.entries(newAuthorsByTopic).reduce((acc, [topicSlug, authors]) => { if (!acc[topicSlug]) { acc[topicSlug] = [] } @@ -81,10 +63,8 @@ export const addAuthorsByTopic = (authorsByTopic: { [topicSlug: string]: Author[ }) return acc - }, authorsByTopicStore.get()) - - authorsByTopicStore.set(newState) - } + }, prevAuthorsByTopic) + }) } export const loadAllAuthors = async (): Promise => { @@ -97,12 +77,7 @@ type InitialState = { } export const useAuthorsStore = (initialState: InitialState = {}) => { - const authors = [...(initialState.authors || [])] - addAuthors(authors) + addAuthors([...(initialState.authors || [])]) - const getAuthorEntities = useStore(authorEntitiesStore) - const getSortedAuthors = useStore(sortedAuthorsStore) - const getAuthorsByTopic = useStore(authorsByTopicStore) - - return { getAuthorEntities, getSortedAuthors, getAuthorsByTopic } + return { authorEntities, sortedAuthors, authorsByTopic } } diff --git a/src/stores/zine/reactions.ts b/src/stores/zine/reactions.ts index 11f38204..7b5dc576 100644 --- a/src/stores/zine/reactions.ts +++ b/src/stores/zine/reactions.ts @@ -40,8 +40,8 @@ export const loadReactions = async ({ limit: number offset: number }): Promise => { - const reactions = await apiClient.getReactionsForShouts({ shoutSlugs, limit, offset }) - reactionsOrdered.set(reactions) + const reactionsForShouts = await apiClient.getReactionsForShouts({ shoutSlugs, limit, offset }) + reactionsOrdered.set(reactionsForShouts) } export const createReaction = async (reaction: Reaction) => diff --git a/src/stores/zine/topAuthors.ts b/src/stores/zine/topAuthors.ts index 16529e69..400313f8 100644 --- a/src/stores/zine/topAuthors.ts +++ b/src/stores/zine/topAuthors.ts @@ -5,20 +5,17 @@ import { useAuthorsStore } from './authors' const TOP_AUTHORS_COUNT = 5 export const useTopAuthorsStore = () => { - const { getArticlesByAuthor } = useArticlesStore() - const { getAuthorEntities } = useAuthorsStore() + const { articlesByAuthor } = useArticlesStore() + const { authorEntities } = useAuthorsStore() - const getTopAuthors = createMemo(() => { - const articlesByAuthor = getArticlesByAuthor() - const authorEntities = getAuthorEntities() - - return Object.keys(articlesByAuthor) + const topAuthors = createMemo(() => { + return Object.keys(articlesByAuthor()) .sort((authorSlug1, authorSlug2) => { - const author1Rating = articlesByAuthor[authorSlug1].reduce( + const author1Rating = articlesByAuthor()[authorSlug1].reduce( (acc, article) => acc + article.stat?.rating, 0 ) - const author2Rating = articlesByAuthor[authorSlug2].reduce( + const author2Rating = articlesByAuthor()[authorSlug2].reduce( (acc, article) => acc + article.stat?.rating, 0 ) @@ -29,9 +26,9 @@ export const useTopAuthorsStore = () => { return author1Rating > author2Rating ? 1 : -1 }) .slice(0, TOP_AUTHORS_COUNT) - .map((authorSlug) => authorEntities[authorSlug]) + .map((authorSlug) => authorEntities()[authorSlug]) .filter(Boolean) }) - return { getTopAuthors } + return { topAuthors } } diff --git a/src/stores/zine/topics.ts b/src/stores/zine/topics.ts index abcd06fb..8994375e 100644 --- a/src/stores/zine/topics.ts +++ b/src/stores/zine/topics.ts @@ -1,66 +1,53 @@ +import { createMemo, createSignal } from 'solid-js' import { apiClient } from '../../utils/apiClient' -import { map, MapStore, ReadableAtom, atom, computed } from 'nanostores' import type { Topic } from '../../graphql/types.gen' -import { useStore } from '@nanostores/solid' import { byCreated, byTopicStatDesc } from '../../utils/sortby' import { getLogger } from '../../utils/logger' -import { createSignal } from 'solid-js' +import { createLazyMemo } from '@solid-primitives/memo' const log = getLogger('topics store') export type TopicsSortBy = 'created' | 'title' | 'authors' | 'shouts' -const sortAllByStore = atom('shouts') +const [sortAllBy, setSortAllBy] = createSignal('shouts') -let topicEntitiesStore: MapStore> -let sortedTopicsStore: ReadableAtom -let topTopicsStore: ReadableAtom +export { setSortAllBy } -const [getRandomTopics, setRandomTopics] = createSignal() -let topicsByAuthorStore: MapStore> +const [topicEntities, setTopicEntities] = createSignal<{ [topicSlug: string]: Topic }>({}) +const [randomTopics, setRandomTopics] = createSignal([]) +const [topicsByAuthor, setTopicByAuthor] = createSignal<{ [authorSlug: string]: Topic[] }>({}) -const initStore = (initial?: { [topicSlug: string]: Topic }) => { - if (topicEntitiesStore) { - return - } +const sortedTopics = createLazyMemo(() => { + const topics = Object.values(topicEntities()) + const sortAllByValue = sortAllBy() - topicEntitiesStore = map>(initial) - - sortedTopicsStore = computed([topicEntitiesStore, sortAllByStore], (topicEntities, sortBy) => { - const topics = Object.values(topicEntities) - switch (sortBy) { - 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}`) + switch (sortAllByValue) { + case 'created': { + // log.debug('sorted by created') + topics.sort(byCreated) + break } - return topics - }) - - topTopicsStore = computed(topicEntitiesStore, (topicEntities) => { - const topics = Object.values(topicEntities) - topics.sort(byTopicStatDesc('shouts')) - return topics - }) -} - -export const setSortAllTopicsBy = (sortBy: TopicsSortBy) => { - if (sortAllByStore.get() !== sortBy) { - sortAllByStore.set(sortBy) + case 'shouts': + case 'authors': + // log.debug(`sorted by ${sortBy}`) + topics.sort(byTopicStatDesc(sortAllByValue)) + break + case 'title': + // log.debug('sorted by title') + topics.sort((a, b) => a.title.localeCompare(b.title)) + break + default: + log.error(`Unknown sort: ${sortAllByValue}`) } -} + + return topics +}) + +const topTopics = createMemo(() => { + const topics = Object.values(topicEntities()) + topics.sort(byTopicStatDesc('shouts')) + return topics +}) const addTopics = (...args: Topic[][]) => { const allTopics = args.flatMap((topics) => topics || []) @@ -70,24 +57,20 @@ const addTopics = (...args: Topic[][]) => { return acc }, {} as Record) - if (!topicEntitiesStore) { - initStore(newTopicEntities) - } else { - topicEntitiesStore.set({ - ...topicEntitiesStore.get(), + setTopicEntities((prevTopicEntities) => { + return { + ...prevTopicEntities, ...newTopicEntities - }) - } + } + }) } -export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic[] }) => { - const allTopics = Object.values(topicsByAuthors).flat() +export const addTopicsByAuthor = (newTopicsByAuthors: { [authorSlug: string]: Topic[] }) => { + const allTopics = Object.values(newTopicsByAuthors).flat() addTopics(allTopics) - if (!topicsByAuthorStore) { - topicsByAuthorStore = map>(topicsByAuthors) - } else { - const newState = Object.entries(topicsByAuthors).reduce((acc, [authorSlug, topics]) => { + setTopicByAuthor((prevTopicsByAuthor) => { + return Object.entries(newTopicsByAuthors).reduce((acc, [authorSlug, topics]) => { if (!acc[authorSlug]) { acc[authorSlug] = [] } @@ -99,10 +82,8 @@ export const addTopicsByAuthor = (topicsByAuthors: { [authorSlug: string]: Topic }) return acc - }, topicsByAuthorStore.get()) - - topicsByAuthorStore.set(newState) - } + }, prevTopicsByAuthor) + }) } export const loadAllTopics = async (): Promise => { @@ -117,24 +98,15 @@ type InitialState = { } export const useTopicsStore = (initialState: InitialState = {}) => { - const topics = [...(initialState.topics || [])] - const randomTopics = [...(initialState.randomTopics || [])] - if (initialState.sortBy) { - sortAllByStore.set(initialState.sortBy) + setSortAllBy(initialState.sortBy) } - addTopics(topics, randomTopics) + addTopics(initialState.topics, initialState.randomTopics) - if (randomTopics) { - setRandomTopics(randomTopics) + if (initialState.randomTopics) { + setRandomTopics(initialState.randomTopics) } - const getTopicEntities = useStore(topicEntitiesStore) - - const getSortedTopics = useStore(sortedTopicsStore) - - const getTopTopics = useStore(topTopicsStore) - - return { getTopicEntities, getSortedTopics, getRandomTopics, getTopTopics } + return { topicEntities, sortedTopics, randomTopics, topTopics, topicsByAuthor } } diff --git a/src/styles/FourOuFour.module.scss b/src/styles/FourOuFour.module.scss index 3ae39ce7..54da05f3 100644 --- a/src/styles/FourOuFour.module.scss +++ b/src/styles/FourOuFour.module.scss @@ -1,11 +1,3 @@ -header { - position: absolute; -} - -footer { - display: none; -} - .main-logo { height: 80px !important; } diff --git a/src/styles/app.scss b/src/styles/app.scss index b78be650..ebca34a5 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -26,7 +26,6 @@ html { height: 100%; -webkit-font-smoothing: antialiased; overscroll-behavior-y: none; - scroll-behavior: smooth; } body { diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 45e605f1..54d5c7e3 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -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 articleBySlug from '../graphql/query/article-by-slug' @@ -36,15 +36,43 @@ const log = getLogger('api-client') const FEED_SIZE = 50 const REACTIONS_PAGE_SIZE = 100 -export const apiClient = { - // auth +type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' - 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 => { 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 }, - authRegiser: async ({ email, password }) => { - const response = await publicGraphQLClient.query(authRegisterMutation, { email, password }).toPromise() + authRegister: async ({ email, password }): Promise => { + const response = await publicGraphQLClient + .mutation(authRegisterMutation, { email, password }) + .toPromise() return response.data.registerUser }, authSignOut: async () => { @@ -87,9 +115,12 @@ export const apiClient = { return response.data.recentPublished }, getRandomTopics: async ({ amount }: { amount: number }) => { - log.debug('getRandomTopics') const response = await publicGraphQLClient.query(topicsRandomQuery, { amount }).toPromise() + if (!response.data) { + log.error('getRandomTopics', response.error) + } + return response.data.topicsRandom }, getSearchResults: async ({ @@ -177,7 +208,7 @@ export const apiClient = { return response.data.unfollow }, - getSession: async () => { + getSession: async (): Promise => { // renew session with auth token in header (!) const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() return response.data.refreshSession diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts new file mode 100644 index 00000000..e3f2d240 --- /dev/null +++ b/src/utils/scroll.ts @@ -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 + }) +} diff --git a/yarn.lock b/yarn.lock index adec712d..162f528d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2733,6 +2733,14 @@ "@solid-primitives/rootless" "^1.1.3" "@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": version "0.0.101" resolved "https://registry.yarnpkg.com/@solid-primitives/platform/-/platform-0.0.101.tgz#7bfa879152a59169589e2dc999aac8ceb63233c7" @@ -2763,6 +2771,11 @@ dependencies: "@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": version "1.0.2" resolved "https://registry.yarnpkg.com/@solid-primitives/scheduled/-/scheduled-1.0.2.tgz#8c2e8511b9b361c22c13e78377dc4168cc9c0452"