diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index ab6d6b07..d521c250 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -7,10 +7,10 @@ import { createMemo, For, onMount, Show } from 'solid-js' import type { Author, Reaction, Shout } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { showModal } from '../../stores/ui' -import { useAuthStore } from '../../stores/auth' import { incrementView } from '../../stores/zine/articles' import MD from './MD' import { SharePopup } from './SharePopup' +import { useSession } from '../../context/session' const MAX_COMMENT_LEVEL = 6 @@ -38,7 +38,7 @@ const formatDate = (date: Date) => { } export const FullArticle = (props: ArticleProps) => { - const { session } = useAuthStore() + const { session } = useSession() onMount(() => { incrementView({ articleSlug: props.article.slug }) diff --git a/src/components/Article/SharePopup.tsx b/src/components/Article/SharePopup.tsx index 851cf599..fe0de743 100644 --- a/src/components/Article/SharePopup.tsx +++ b/src/components/Article/SharePopup.tsx @@ -1,7 +1,9 @@ import { Icon } from '../Nav/Icon' -import styles from '../Nav/Popup.module.scss' import { t } from '../../utils/intl' -import { Popup, PopupProps } from '../Nav/Popup' + +import styles from '../_shared/Popup.module.scss' +import type { PopupProps } from '../_shared/Popup' +import { Popup } from '../_shared/Popup' type SharePopupProps = Omit diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index ad75417e..b51d4c13 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -15,9 +15,10 @@ .authorDetails { display: flex; flex: 1; - //padding-right: 1.2rem; width: max-content; + // padding-right: 1.2rem; + @include media-breakpoint-down(sm) { flex-wrap: wrap; } @@ -242,6 +243,7 @@ .authorsListItem { .authorName { @include font-size(2.2rem); + font-weight: bold; } diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx index 4477ee34..4115eda1 100644 --- a/src/components/Author/Card.tsx +++ b/src/components/Author/Card.tsx @@ -5,10 +5,10 @@ import styles from './Card.module.scss' import { createMemo, For, Show } from 'solid-js' import { translit } from '../../utils/ru2en' import { t } from '../../utils/intl' -import { useAuthStore } from '../../stores/auth' import { locale } from '../../stores/ui' import { follow, unfollow } from '../../stores/zine/common' import { clsx } from 'clsx' +import { useSession } from '../../context/session' interface AuthorCardProps { compact?: boolean @@ -23,7 +23,7 @@ interface AuthorCardProps { } export const AuthorCard = (props: AuthorCardProps) => { - const { session } = useAuthStore() + const { session } = useSession() const subscribed = createMemo( () => session()?.news?.authors?.some((u) => u === props.author.slug) || false diff --git a/src/components/Feed/Sidebar.tsx b/src/components/Feed/Sidebar.tsx index 09dac4a8..1dc4edc1 100644 --- a/src/components/Feed/Sidebar.tsx +++ b/src/components/Feed/Sidebar.tsx @@ -1,12 +1,12 @@ import { For } from 'solid-js' import type { Author } from '../../graphql/types.gen' -import { useAuthStore } from '../../stores/auth' import { useAuthorsStore } from '../../stores/zine/authors' import { t } from '../../utils/intl' import { Icon } from '../Nav/Icon' import { useTopicsStore } from '../../stores/zine/topics' import { useArticlesStore } from '../../stores/zine/articles' import { useSeenStore } from '../../stores/zine/seen' +import { useSession } from '../../context/session' type FeedSidebarProps = { authors: Author[] @@ -14,7 +14,7 @@ type FeedSidebarProps = { export const FeedSidebar = (props: FeedSidebarProps) => { const { getSeen: seen } = useSeenStore() - const { session } = useAuthStore() + const { session } = useSession() const { authorEntities } = useAuthorsStore({ authors: props.authors }) const { articlesByTopic } = useArticlesStore() const { topicEntities } = useTopicsStore() diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index 1fd1c7aa..b734d23d 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -2,16 +2,20 @@ import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { t } from '../../../utils/intl' import { hideModal } from '../../../stores/ui' -import { createMemo, onMount, Show } from 'solid-js' -import { useRouter } from '../../../stores/router' -import { confirmEmail, useAuthStore } from '../../../stores/auth' - -type ConfirmEmailSearchParams = { - token: string -} +import { createMemo, createSignal, onMount, Show } from 'solid-js' +import { handleClientRouteLinkClick, useRouter } from '../../../stores/router' +import type { ConfirmEmailSearchParams } from './types' +import { ApiError } from '../../../utils/apiClient' +import { useSession } from '../../../context/session' export const EmailConfirm = () => { - const { session } = useAuthStore() + const { + session, + actions: { confirmEmail } + } = useSession() + + const [isTokenExpired, setIsTokenExpired] = createSignal(false) + const [isTokenInvalid, setIsTokenInvalid] = createSignal(false) const confirmedEmail = createMemo(() => session()?.user?.email || '') @@ -22,23 +26,54 @@ export const EmailConfirm = () => { try { await confirmEmail(token) } catch (error) { + if (error instanceof ApiError) { + if (error.code === 'token_expired') { + setIsTokenExpired(true) + return + } + + if (error.code === 'token_invalid') { + setIsTokenInvalid(true) + return + } + } + console.log(error) } }) return (
-
{t('Hooray! Welcome!')}
+ {/* TODO: texts */} + +
Ссылка больше не действительна
+ +
+ +
Неправильная ссылка
+ +
+
{t('Hooray! Welcome!')}
{t("You've confirmed email")} {confirmedEmail()}
+
+ +
-
- -
) } diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index f19a89f4..6a92582b 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -6,8 +6,9 @@ import { useRouter } from '../../../stores/router' import { email, setEmail } from './sharedLogic' import type { AuthModalSearchParams } from './types' import { isValidEmail } from './validators' -import { signSendLink } from '../../../stores/auth' import { locale } from '../../../stores/ui' +import { ApiError } from '../../../utils/apiClient' +import { signSendLink } from '../../../stores/auth' type FormFields = { email: string @@ -26,11 +27,13 @@ export const ForgotPasswordForm = () => { const [submitError, setSubmitError] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) const [validationErrors, setValidationErrors] = createSignal({}) + const [isUserNotFount, setIsUserNotFound] = createSignal(false) const handleSubmit = async (event: Event) => { event.preventDefault() setSubmitError('') + setIsUserNotFound(false) const newValidationErrors: ValidationErrors = {} @@ -51,9 +54,12 @@ export const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const result = await signSendLink({ email: email(), lang: locale() }) - if (result.error) setSubmitError(result.error) + await signSendLink({ email: email(), lang: locale() }) } catch (error) { + if (error instanceof ApiError && error.code === 'user_not_found') { + setIsUserNotFound(true) + return + } setSubmitError(error.message) } finally { setIsSubmitting(false) @@ -71,6 +77,21 @@ export const ForgotPasswordForm = () => { + +
+ {/*TODO: text*/} + {t("We can't find you, check email or")}{' '} + { + event.preventDefault() + changeSearchParam('mode', 'register') + }} + > + {t('register')} + +
+
{validationErrors().email}
diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index f098f392..89f98d66 100644 --- a/src/components/Nav/AuthModal/LoginForm.tsx +++ b/src/components/Nav/AuthModal/LoginForm.tsx @@ -2,7 +2,6 @@ import { t } from '../../../utils/intl' import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { SocialProviders } from './SocialProviders' -import { signIn, signSendLink } from '../../../stores/auth' import { ApiError } from '../../../utils/apiClient' import { createSignal, Show } from 'solid-js' import { isValidEmail } from './validators' @@ -10,6 +9,8 @@ import { email, setEmail } from './sharedLogic' import { useRouter } from '../../../stores/router' import type { AuthModalSearchParams } from './types' import { hideModal, locale } from '../../../stores/ui' +import { useSession } from '../../../context/session' +import { signSendLink } from '../../../stores/auth' type FormFields = { email: string @@ -26,6 +27,10 @@ export const LoginForm = () => { const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) const [isLinkSent, setIsLinkSent] = createSignal(false) + const { + actions: { signIn } + } = useSession() + const { changeSearchParam } = useRouter() const [password, setPassword] = createSignal('') @@ -53,6 +58,7 @@ export const LoginForm = () => { event.preventDefault() setIsLinkSent(false) + setIsEmailNotConfirmed(false) setSubmitError('') const newValidationErrors: ValidationErrors = {} diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 3e464b05..b5f56fb7 100644 --- a/src/components/Nav/AuthModal/RegisterForm.tsx +++ b/src/components/Nav/AuthModal/RegisterForm.tsx @@ -4,13 +4,14 @@ import { t } from '../../../utils/intl' import styles from './AuthModal.module.scss' import { clsx } from 'clsx' import { SocialProviders } from './SocialProviders' -import { checkEmail, register, useAuthStore } from '../../../stores/auth' import { isValidEmail } from './validators' import { ApiError } from '../../../utils/apiClient' import { email, setEmail } from './sharedLogic' import { useRouter } from '../../../stores/router' import type { AuthModalSearchParams } from './types' import { hideModal } from '../../../stores/ui' +import { checkEmail, useEmailChecks } from '../../../stores/emailChecks' +import { register } from '../../../stores/auth' type FormFields = { name: string @@ -23,7 +24,7 @@ type ValidationErrors = Partial> export const RegisterForm = () => { const { changeSearchParam } = useRouter() - const { emailChecks } = useAuthStore() + const { emailChecks } = useEmailChecks() const [submitError, setSubmitError] = createSignal('') const [name, setName] = createSignal('') @@ -60,11 +61,14 @@ export const RegisterForm = () => { const newValidationErrors: ValidationErrors = {} - if (!name()) { + const clearName = name().trim() + const clearEmail = email().trim() + + if (!clearName) { newValidationErrors.name = t('Please enter a name to sign your comments and publication') } - if (!email()) { + if (!clearEmail) { newValidationErrors.email = t('Please enter email') } else if (!isValidEmail(email())) { newValidationErrors.email = t('Invalid email') @@ -76,7 +80,7 @@ export const RegisterForm = () => { setValidationErrors(newValidationErrors) - const emailCheckResult = await checkEmail(email()) + const emailCheckResult = await checkEmail(clearEmail) const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult @@ -88,8 +92,8 @@ export const RegisterForm = () => { try { await register({ - name: name(), - email: email(), + name: clearName, + email: clearEmail, password: password() }) diff --git a/src/components/Nav/AuthModal/types.ts b/src/components/Nav/AuthModal/types.ts index e7aeed0e..c8c0add6 100644 --- a/src/components/Nav/AuthModal/types.ts +++ b/src/components/Nav/AuthModal/types.ts @@ -3,3 +3,7 @@ export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-pas export type AuthModalSearchParams = { mode: AuthModalMode } + +export type ConfirmEmailSearchParams = { + token: string +} diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index 904fdafe..adbe721c 100644 --- a/src/components/Nav/Header.tsx +++ b/src/components/Nav/Header.tsx @@ -1,22 +1,15 @@ -import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js' -import Notifications from './Notifications' +import { For, Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js' import { Icon } from './Icon' import { Modal } from './Modal' import { AuthModal } from './AuthModal' import { t } from '../../utils/intl' -import { useModalStore, showModal, useWarningsStore } from '../../stores/ui' -import { useAuthStore } from '../../stores/auth' +import { useModalStore } from '../../stores/ui' import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router' import styles from './Header.module.scss' import { getPagePath } from '@nanostores/router' -import { getLogger } from '../../utils/logger' import { clsx } from 'clsx' +import { HeaderAuth } from './HeaderAuth' import { SharePopup } from '../Article/SharePopup' -import { ProfilePopup } from './ProfilePopup' -import Userpic from '../Author/Userpic' -import type { Author } from '../../graphql/types.gen' - -const log = getLogger('header') const resources: { name: string; route: keyof Routes }[] = [ { name: t('zine'), route: 'home' }, @@ -34,19 +27,15 @@ export const Header = (props: Props) => { const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false) const [getIsScrolled, setIsScrolled] = createSignal(false) const [fixed, setFixed] = createSignal(false) - const [visibleWarnings, setVisibleWarnings] = createSignal(false) const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false) const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false) - // stores - const { warnings } = useWarningsStore() - const { session } = useAuthStore() const { modal } = useModalStore() const { page } = useRouter() // methods - const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) + const toggleFixed = () => setFixed((oldFixed) => !oldFixed) // effects @@ -69,20 +58,6 @@ export const Header = (props: Props) => { } }) - // derived - const authorized = createMemo(() => session()?.user?.slug) - - const handleBellIconClick = (event: Event) => { - event.preventDefault() - - if (!authorized()) { - showModal('auth') - return - } - - toggleWarnings() - } - onMount(() => { let scrollTop = window.scrollY @@ -146,88 +121,27 @@ export const Header = (props: Props) => { -
- - } - > - - { - setIsProfilePopupVisible(isVisible) - }} - containerCssClass={styles.control} - trigger={ -
- -
- } - /> - + + +
+ { + setIsSharePopupVisible(isVisible) + }} + containerCssClass={styles.control} + trigger={} + /> + + + + event.preventDefault()}> + + + event.preventDefault()}> + +
- -
- { - setIsSharePopupVisible(isVisible) - }} - containerCssClass={styles.control} - trigger={} - /> - - - - event.preventDefault()}> - - - event.preventDefault()}> - - -
-
-
+
diff --git a/src/components/Nav/HeaderAuth.tsx b/src/components/Nav/HeaderAuth.tsx new file mode 100644 index 00000000..a98566d8 --- /dev/null +++ b/src/components/Nav/HeaderAuth.tsx @@ -0,0 +1,107 @@ +import styles from './Header.module.scss' +import { clsx } from 'clsx' +import { handleClientRouteLinkClick, useRouter } from '../../stores/router' +import { t } from '../../utils/intl' +import { Icon } from './Icon' +import { createSignal, onMount, Show } from 'solid-js' +import Notifications from './Notifications' +import { ProfilePopup } from './ProfilePopup' +import Userpic from '../Author/Userpic' +import type { Author } from '../../graphql/types.gen' +import { showModal, useWarningsStore } from '../../stores/ui' +import { ClientContainer } from '../_shared/ClientContainer' +import { useSession } from '../../context/session' + +type HeaderAuthProps = { + setIsProfilePopupVisible: (value: boolean) => void +} + +export const HeaderAuth = (props: HeaderAuthProps) => { + const { page } = useRouter() + const [visibleWarnings, setVisibleWarnings] = createSignal(false) + const { warnings } = useWarningsStore() + + const { session, isAuthenticated } = useSession() + + const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) + + const handleBellIconClick = (event: Event) => { + event.preventDefault() + + if (!isAuthenticated()) { + showModal('auth') + return + } + + toggleWarnings() + } + + return ( + + +
+ + } + > + + { + props.setIsProfilePopupVisible(isVisible) + }} + containerCssClass={styles.control} + trigger={ +
+ +
+ } + /> + +
+
+ + + ) +} diff --git a/src/components/Nav/ProfileModal.tsx b/src/components/Nav/ProfileModal.tsx index 2beab06f..ddfabe87 100644 --- a/src/components/Nav/ProfileModal.tsx +++ b/src/components/Nav/ProfileModal.tsx @@ -2,16 +2,19 @@ import { AuthorCard } from '../Author/Card' import type { Author } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { hideModal } from '../../stores/ui' -import { useAuthStore, signOut } from '../../stores/auth' import { createMemo, For } from 'solid-js' +import { useSession } from '../../context/session' -const quit = () => { - signOut() - hideModal() -} +export const ProfileModal = () => { + const { + session, + actions: { signOut } + } = useSession() -export default () => { - const { session } = useAuthStore() + const quit = () => { + signOut() + hideModal() + } const author = createMemo(() => { const a: Author = { diff --git a/src/components/Nav/ProfilePopup.tsx b/src/components/Nav/ProfilePopup.tsx index 204034d0..807b0cdd 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -1,11 +1,15 @@ -import { Popup, PopupProps } from './Popup' -import { signOut, useAuthStore } from '../../stores/auth' -import styles from './Popup.module.scss' +import { useSession } from '../../context/session' +import type { PopupProps } from '../_shared/Popup' +import { Popup } from '../_shared/Popup' +import styles from '../_shared/Popup.module.scss' type ProfilePopupProps = Omit export const ProfilePopup = (props: ProfilePopupProps) => { - const { session } = useAuthStore() + const { + session, + actions: { signOut } + } = useSession() return ( diff --git a/src/components/Nav/Topics.tsx b/src/components/Nav/Topics.tsx index 14750cf9..5ce88cce 100644 --- a/src/components/Nav/Topics.tsx +++ b/src/components/Nav/Topics.tsx @@ -4,6 +4,7 @@ import { Icon } from './Icon' import './Topics.scss' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' +import { handleClientRouteLinkClick } from '../../stores/router' export const NavTopics = (props: { topics: Topic[] }) => { const tag = (topic: Topic) => @@ -17,7 +18,7 @@ export const NavTopics = (props: { topics: Topic[] }) => { {(topic) => (
  • - + #{tag(topic)}
  • diff --git a/src/components/Pages/TopicPage.tsx b/src/components/Pages/TopicPage.tsx index 16e0c95a..35a94c92 100644 --- a/src/components/Pages/TopicPage.tsx +++ b/src/components/Pages/TopicPage.tsx @@ -8,7 +8,7 @@ import { loadTopic } from '../../stores/zine/topics' import { Loading } from '../Loading' export const TopicPage = (props: PageProps) => { - const [isLoaded, setIsLoaded] = createSignal(Boolean(props.authorArticles) && Boolean(props.author)) + const [isLoaded, setIsLoaded] = createSignal(Boolean(props.topicArticles) && Boolean(props.topic)) const slug = createMemo(() => { const { page: getPage } = useRouter() diff --git a/src/components/Root.tsx b/src/components/Root.tsx index fc1a7ff9..46efc845 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -2,12 +2,11 @@ // import 'solid-devtools' import { MODALS, setLocale, showModal } from '../stores/ui' -import { Component, createEffect, createMemo, onMount } 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' +import type { PageProps, RootSearchParams } from './types' import { HomePage } from './Pages/HomePage' import { AllTopicsPage } from './Pages/AllTopicsPage' @@ -30,7 +29,7 @@ import { TermsOfUsePage } from './Pages/about/TermsOfUsePage' import { ThanksPage } from './Pages/about/ThanksPage' import { CreatePage } from './Pages/CreatePage' import { ConnectPage } from './Pages/ConnectPage' -import { renewSession } from '../stores/auth' +import { SessionProvider } from '../context/session' // TODO: lazy load // const HomePage = lazy(() => import('./Pages/HomePage')) @@ -52,13 +51,6 @@ import { renewSession } from '../stores/auth' // const ThanksPage = lazy(() => import('./Pages/about/ThanksPage')) // const CreatePage = lazy(() => import('./Pages/about/CreatePage')) -const log = getLogger('root') - -type RootSearchParams = { - modal: string - lang: string -} - const pagesMap: Record> = { connect: ConnectPage, create: CreatePage, @@ -92,10 +84,6 @@ export const Root = (props: PageProps) => { } }) - onMount(() => { - renewSession() - }) - const pageComponent = createMemo(() => { const result = pagesMap[page().route] @@ -114,5 +102,9 @@ export const Root = (props: PageProps) => { }) } - return + return ( + + + + ) } diff --git a/src/components/Topic/Card.tsx b/src/components/Topic/Card.tsx index 16594b58..0b153cbe 100644 --- a/src/components/Topic/Card.tsx +++ b/src/components/Topic/Card.tsx @@ -5,10 +5,10 @@ import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' -import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { getLogger } from '../../utils/logger' import { clsx } from 'clsx' +import { useSession } from '../../context/session' const log = getLogger('TopicCard') @@ -24,7 +24,7 @@ interface TopicProps { } export const TopicCard = (props: TopicProps) => { - const { session } = useAuthStore() + const { session } = useSession() const subscribed = createMemo(() => { if (!session()?.user?.slug || !session()?.news?.topics) { diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index c6bb57fa..86c48852 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -2,17 +2,17 @@ import { createMemo, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { FollowingEntity } from '../../graphql/types.gen' import styles from './Full.module.scss' -import { useAuthStore } from '../../stores/auth' import { follow, unfollow } from '../../stores/zine/common' import { t } from '../../utils/intl' import { clsx } from 'clsx' +import { useSession } from '../../context/session' type Props = { topic: Topic } export const FullTopic = (props: Props) => { - const { session } = useAuthStore() + const { session } = useSession() const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug)) return ( diff --git a/src/components/Views/AllAuthors.tsx b/src/components/Views/AllAuthors.tsx index 2ba33ed0..99fedb64 100644 --- a/src/components/Views/AllAuthors.tsx +++ b/src/components/Views/AllAuthors.tsx @@ -1,13 +1,13 @@ -import { createEffect, createMemo, For, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import type { Author } from '../../graphql/types.gen' import { AuthorCard } from '../Author/Card' import { Icon } from '../Nav/Icon' import { t } from '../../utils/intl' import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' -import { useAuthStore } from '../../stores/auth' import styles from '../../styles/AllTopics.module.scss' import { clsx } from 'clsx' +import { useSession } from '../../context/session' type AllAuthorsPageSearchParams = { by: '' | 'name' | 'shouts' | 'rating' @@ -17,10 +17,13 @@ type Props = { authors: Author[] } +const PAGE_SIZE = 20 + export const AllAuthorsView = (props: Props) => { const { sortedAuthors } = useAuthorsStore({ authors: props.authors }) + const [limit, setLimit] = createSignal(PAGE_SIZE) - const { session } = useAuthStore() + const { session } = useSession() createEffect(() => { setAuthorsSort(searchParams().by || 'shouts') @@ -54,7 +57,7 @@ export const AllAuthorsView = (props: Props) => { return keys }) - // log.debug(getSearchParams()) + const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) return (
    @@ -95,7 +98,7 @@ export const AllAuthorsView = (props: Props) => { when={!searchParams().by || searchParams().by === 'name'} fallback={() => (
    - + {(author) => ( { /> )} + limit()}> +
    + +
    +
    )} > diff --git a/src/components/Views/AllTopics.tsx b/src/components/Views/AllTopics.tsx index 687385eb..d94461f4 100644 --- a/src/components/Views/AllTopics.tsx +++ b/src/components/Views/AllTopics.tsx @@ -1,14 +1,13 @@ -import { createEffect, createMemo, For, Show } from 'solid-js' +import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' import { Icon } from '../Nav/Icon' import { t } from '../../utils/intl' import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { handleClientRouteLinkClick, useRouter } from '../../stores/router' import { TopicCard } from '../Topic/Card' -import { useAuthStore } from '../../stores/auth' import styles from '../../styles/AllTopics.module.scss' -import cardStyles from '../Topic/Card.module.scss' import { clsx } from 'clsx' +import { useSession } from '../../context/session' type AllTopicsPageSearchParams = { by: 'shouts' | 'authors' | 'title' | '' @@ -18,18 +17,22 @@ type AllTopicsViewProps = { topics: Topic[] } +const PAGE_SIZE = 20 + export const AllTopicsView = (props: AllTopicsViewProps) => { const { searchParams, changeSearchParam } = useRouter() + const [limit, setLimit] = createSignal(PAGE_SIZE) const { sortedTopics } = useTopicsStore({ topics: props.topics, sortBy: searchParams().by || 'shouts' }) - const { session } = useAuthStore() + const { session } = useSession() createEffect(() => { setTopicsSort(searchParams().by || 'shouts') + setLimit(PAGE_SIZE) }) const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => { @@ -53,6 +56,8 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) + const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) + return (
    0}> @@ -102,9 +107,20 @@ export const AllTopicsView = (props: AllTopicsViewProps) => { ( - - {(topic) => } - + <> + + {(topic) => ( + + )} + + limit()}> +
    + +
    +
    + )} > diff --git a/src/components/Views/Create.tsx b/src/components/Views/Create.tsx index 2281ddf5..636b1386 100644 --- a/src/components/Views/Create.tsx +++ b/src/components/Views/Create.tsx @@ -1,17 +1,11 @@ -import { Show, onMount, createSignal } from 'solid-js' import { Editor } from '../EditorNew/Editor' +import { ClientContainer } from '../_shared/ClientContainer' export const CreateView = () => { - // don't render anything on server - // usage of isServer causing hydration errors - const [isMounted, setIsMounted] = createSignal(false) - - onMount(() => setIsMounted(true)) - return ( - + - + ) } diff --git a/src/components/Views/Feed.tsx b/src/components/Views/Feed.tsx index c791331c..aca7987b 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -8,13 +8,13 @@ import { ArticleCard } from '../Feed/Card' import { AuthorCard } from '../Author/Card' import { t } from '../../utils/intl' import { FeedSidebar } from '../Feed/Sidebar' -import { useAuthStore } from '../../stores/auth' import CommentCard from '../Article/Comment' import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles' import { useReactionsStore } from '../../stores/zine/reactions' import { useAuthorsStore } from '../../stores/zine/authors' import { useTopicsStore } from '../../stores/zine/topics' import { useTopAuthorsStore } from '../../stores/zine/topAuthors' +import { useSession } from '../../context/session' // const AUTHORSHIP_REACTIONS = [ // ReactionKind.Accept, @@ -32,7 +32,7 @@ export const FeedView = () => { const { sortedAuthors } = useAuthorsStore() const { topTopics } = useTopicsStore() const { topAuthors } = useTopAuthorsStore() - const { session } = useAuthStore() + const { session } = useSession() const topReactions = createMemo(() => sortBy(reactions(), byCreated)) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index cfe9143b..fca408f4 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -8,7 +8,7 @@ import { FullTopic } from '../Topic/Full' import { t } from '../../utils/intl' import { useRouter } from '../../stores/router' import { useTopicsStore } from '../../stores/zine/topics' -import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles' +import { loadTopicArticles, useArticlesStore } from '../../stores/zine/articles' import { useAuthorsStore } from '../../stores/zine/authors' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { splitToPages } from '../../utils/splitToPages' @@ -44,7 +44,8 @@ export const TopicView = (props: TopicProps) => { const loadMore = async () => { saveScrollPosition() - const { hasMore } = await loadPublishedArticles({ + const { hasMore } = await loadTopicArticles({ + topicSlug: topic().slug, limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length }) @@ -130,20 +131,18 @@ export const TopicView = (props: TopicProps) => { wrapper={'top-article'} /> - 5}> - - + + - + - - - + + {(page) => ( diff --git a/src/components/_shared/ClientContainer.tsx b/src/components/_shared/ClientContainer.tsx new file mode 100644 index 00000000..913efae6 --- /dev/null +++ b/src/components/_shared/ClientContainer.tsx @@ -0,0 +1,12 @@ +import type { JSX } from 'solid-js' +import { createSignal, onMount, Show } from 'solid-js' + +// show children only on client side +// usage of isServer causing hydration errors +export const ClientContainer = (props: { children: JSX.Element }) => { + const [isMounted, setIsMounted] = createSignal(false) + + onMount(() => setIsMounted(true)) + + return {props.children} +} diff --git a/src/components/Nav/Popup.module.scss b/src/components/_shared/Popup.module.scss similarity index 100% rename from src/components/Nav/Popup.module.scss rename to src/components/_shared/Popup.module.scss diff --git a/src/components/Nav/Popup.tsx b/src/components/_shared/Popup.tsx similarity index 100% rename from src/components/Nav/Popup.tsx rename to src/components/_shared/Popup.tsx diff --git a/src/components/types.ts b/src/components/types.ts index f3158bb7..866bb4f5 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -17,3 +17,8 @@ export type PageProps = { searchResults?: Shout[] chats?: Chat[] } + +export type RootSearchParams = { + modal: string + lang: string +} diff --git a/src/context/session.tsx b/src/context/session.tsx new file mode 100644 index 00000000..47a51e76 --- /dev/null +++ b/src/context/session.tsx @@ -0,0 +1,81 @@ +import type { Accessor, InitializedResource, JSX } from 'solid-js' +import { createContext, createMemo, createResource, onMount, useContext } from 'solid-js' +import type { AuthResult } from '../graphql/types.gen' +import { apiClient } from '../utils/apiClient' +import { resetToken, setToken } from '../graphql/privateGraphQLClient' + +type SessionContextType = { + session: InitializedResource + isAuthenticated: Accessor + actions: { + refreshSession: () => AuthResult | Promise + signIn: ({ email, password }: { email: string; password: string }) => Promise + signOut: () => Promise + confirmEmail: (token: string) => Promise + } +} + +const SessionContext = createContext() + +const refreshSession = async (): Promise => { + try { + const authResult = await apiClient.getSession() + if (!authResult) { + return null + } + setToken(authResult.token) + return authResult + } catch (error) { + console.error('renewSession error:', error) + resetToken() + return null + } +} + +export function useSession() { + return useContext(SessionContext) +} + +export const SessionProvider = (props: { children: JSX.Element }) => { + const [session, { refetch: refetchRefreshSession, mutate }] = createResource(refreshSession, { + ssrLoadFrom: 'initial', + initialValue: null + }) + + const isAuthenticated = createMemo(() => Boolean(session()?.user?.slug)) + + const signIn = async ({ email, password }: { email: string; password: string }) => { + const authResult = await apiClient.authLogin({ email, password }) + mutate(authResult) + setToken(authResult.token) + console.debug('signed in') + } + + const signOut = async () => { + // TODO: call backend to revoke token + mutate(null) + resetToken() + console.debug('signed out') + } + + const confirmEmail = async (token: string) => { + const authResult = await apiClient.confirmEmail({ token }) + mutate(authResult) + setToken(authResult.token) + } + + const actions = { + refreshSession: refetchRefreshSession, + signIn, + signOut, + confirmEmail + } + + const value: SessionContextType = { session, isAuthenticated, actions } + + onMount(() => { + refetchRefreshSession() + }) + + return {props.children} +} diff --git a/src/graphql/privateGraphQLClient.ts b/src/graphql/privateGraphQLClient.ts index dc9ff986..442343cf 100644 --- a/src/graphql/privateGraphQLClient.ts +++ b/src/graphql/privateGraphQLClient.ts @@ -10,6 +10,10 @@ if (isDev) { exchanges.unshift(devtoolsExchange) } +export const getToken = (): string => { + return localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY) +} + export const setToken = (token: string) => { localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token) } @@ -27,7 +31,6 @@ const options: ClientOptions = { // меняем через setToken, например при получении значения с сервера // скорее всего придумаем что-нибудь получше со временем const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY) - const headers = { Auth: token } return { headers } }, diff --git a/src/graphql/query/author-by-slug.ts b/src/graphql/query/author-by-slug.ts new file mode 100644 index 00000000..bf35df61 --- /dev/null +++ b/src/graphql/query/author-by-slug.ts @@ -0,0 +1,22 @@ +import { gql } from '@urql/core' + +export default gql` + query GetAuthorBySlugQuery($slug: String!) { + getAuthor(slug: $slug) { + _id: slug + slug + name + bio + userpic + communities + links + createdAt + lastSeen + ratings { + _id: rater + rater + value + } + } + } +` diff --git a/src/graphql/query/authors-all.ts b/src/graphql/query/authors-all.ts index 4570a611..4b02a3a5 100644 --- a/src/graphql/query/authors-all.ts +++ b/src/graphql/query/authors-all.ts @@ -8,14 +8,11 @@ export default gql` name bio userpic - communities links - createdAt lastSeen - ratings { - _id: rater - rater - value + stat { + followers + followings } } } diff --git a/src/graphql/query/topic-by-slug.ts b/src/graphql/query/topic-by-slug.ts new file mode 100644 index 00000000..0e440496 --- /dev/null +++ b/src/graphql/query/topic-by-slug.ts @@ -0,0 +1,22 @@ +import { gql } from '@urql/core' + +export default gql` + query TopicBySlugQuery($slug: String!) { + getTopic(slug: $slug) { + title + body + slug + pic + parents + children + # community + stat { + _id: shouts + shouts + authors + # viewed + followers + } + } + } +` diff --git a/src/graphql/types.gen.ts b/src/graphql/types.gen.ts index 7be17aa5..8f2e7454 100644 --- a/src/graphql/types.gen.ts +++ b/src/graphql/types.gen.ts @@ -25,18 +25,30 @@ export type Author = { bio?: Maybe caption?: Maybe id: Scalars['Int'] + lastSeen?: Maybe links?: Maybe>> name: Scalars['String'] + roles?: Maybe>> slug: Scalars['String'] + stat?: Maybe userpic?: Maybe } +export type AuthorStat = { + commented?: Maybe + followers?: Maybe + followings?: Maybe + rating?: Maybe +} + export type Chat = { + admins?: Maybe>> createdAt: Scalars['Int'] createdBy: User description?: Maybe id: Scalars['String'] messages: Array> + private?: Maybe title?: Maybe unread?: Maybe updatedAt: Scalars['Int'] @@ -53,8 +65,8 @@ export type ChatMember = { invitedAt?: Maybe invitedBy?: Maybe name: Scalars['String'] - pic?: Maybe slug: Scalars['String'] + userpic?: Maybe } export type Collab = { @@ -130,6 +142,7 @@ export type Mutation = { createReaction: Result createShout: Result createTopic: Result + deleteChat: Result deleteCollection: Result deleteCommunity: Result deleteMessage: Result @@ -193,6 +206,10 @@ export type MutationCreateTopicArgs = { input: TopicInput } +export type MutationDeleteChatArgs = { + chatId: Scalars['String'] +} + export type MutationDeleteCollectionArgs = { slug: Scalars['String'] } @@ -330,7 +347,7 @@ export type ProfileInput = { } export type Query = { - authorsAll: Array> + authorsAll: Array> collectionsAll: Array> getAuthor: User getCollabs: Array> @@ -340,39 +357,46 @@ export type Query = { getTopic: Topic getUserCollections: Array> getUserRoles: Array> - getUsersBySlugs: Array> + getUsersBySlugs: Array> isEmailUsed: Scalars['Boolean'] - loadChat: Result + loadChats: Result + loadMessages: Result markdownBody: Scalars['String'] - myChats: Result reactionsByAuthor: Array> reactionsForShouts: Array> recentAll: Array> recentCandidates: Array> recentCommented: Array> + recentLayoutShouts: Array> recentPublished: Array> recentReacted: Array> + searchChats: Result + searchMessages: Result searchQuery?: Maybe>> + searchUsers: Result shoutsByAuthors: Array> shoutsByCollection: Array> shoutsByCommunities: Array> + shoutsByLayout: Array> shoutsByTopics: Array> shoutsForFeed: Array> signIn: AuthResult signOut: AuthResult topAuthors: Array> topCommented: Array> + topLayoutShouts: Array> topMonth: Array> + topMonthLayoutShouts: Array> topOverall: Array> topPublished: Array> topicsAll: Array> topicsByAuthor: Array> topicsByCommunity: Array> topicsRandom: Array> - userFollowedAuthors: Array> + userFollowedAuthors: Array> userFollowedCommunities: Array> userFollowedTopics: Array> - userFollowers: Array> + userFollowers: Array> userReactedShouts: Array> } @@ -408,7 +432,12 @@ export type QueryIsEmailUsedArgs = { email: Scalars['String'] } -export type QueryLoadChatArgs = { +export type QueryLoadChatsArgs = { + amount?: InputMaybe + offset?: InputMaybe +} + +export type QueryLoadMessagesArgs = { amount?: InputMaybe chatId: Scalars['String'] offset?: InputMaybe @@ -445,6 +474,12 @@ export type QueryRecentCommentedArgs = { offset: Scalars['Int'] } +export type QueryRecentLayoutShoutsArgs = { + amount?: InputMaybe + layout: Scalars['String'] + offset?: InputMaybe +} + export type QueryRecentPublishedArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -455,12 +490,30 @@ export type QueryRecentReactedArgs = { offset: Scalars['Int'] } +export type QuerySearchChatsArgs = { + amount?: InputMaybe + offset?: InputMaybe + q: Scalars['String'] +} + +export type QuerySearchMessagesArgs = { + amount?: InputMaybe + offset?: InputMaybe + q: Scalars['String'] +} + export type QuerySearchQueryArgs = { limit: Scalars['Int'] offset: Scalars['Int'] q?: InputMaybe } +export type QuerySearchUsersArgs = { + amount?: InputMaybe + offset?: InputMaybe + q: Scalars['String'] +} + export type QueryShoutsByAuthorsArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -479,6 +532,12 @@ export type QueryShoutsByCommunitiesArgs = { slugs: Array> } +export type QueryShoutsByLayoutArgs = { + amount: Scalars['Int'] + layout?: InputMaybe + offset: Scalars['Int'] +} + export type QueryShoutsByTopicsArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -506,11 +565,23 @@ export type QueryTopCommentedArgs = { offset: Scalars['Int'] } +export type QueryTopLayoutShoutsArgs = { + amount?: InputMaybe + layout: Scalars['String'] + offset?: InputMaybe +} + export type QueryTopMonthArgs = { limit: Scalars['Int'] offset: Scalars['Int'] } +export type QueryTopMonthLayoutShoutsArgs = { + amount?: InputMaybe + layout: Scalars['String'] + offset?: InputMaybe +} + export type QueryTopOverallArgs = { limit: Scalars['Int'] offset: Scalars['Int'] @@ -619,8 +690,8 @@ export type Resource = { } export type Result = { - author?: Maybe - authors?: Maybe>> + author?: Maybe + authors?: Maybe>> chat?: Maybe chats?: Maybe>> communities?: Maybe>> @@ -633,8 +704,10 @@ export type Result = { reactions?: Maybe>> shout?: Maybe shouts?: Maybe>> + slugs?: Maybe>> topic?: Maybe topics?: Maybe>> + uids?: Maybe>> } export type Role = { @@ -653,11 +726,11 @@ export type Shout = { createdAt: Scalars['DateTime'] deletedAt?: Maybe deletedBy?: Maybe - draft?: Maybe id: Scalars['Int'] lang?: Maybe layout?: Maybe mainTopic?: Maybe + media?: Maybe publishedAt?: Maybe publishedBy?: Maybe slug: Scalars['String'] @@ -667,8 +740,8 @@ export type Shout = { topics?: Maybe>> updatedAt?: Maybe updatedBy?: Maybe - versionOf?: Maybe - visibleFor?: Maybe>> + versionOf?: Maybe + visibility?: Maybe } export type ShoutInput = { diff --git a/src/locales/ru.json b/src/locales/ru.json index e8b1a94d..a7644c70 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -169,5 +169,7 @@ "Send link again": "Прислать ссылку ещё раз", "Link sent, check your email": "Ссылка отправлена, проверьте почту", "Create post": "Создать публикацию", - "Just start typing...": "Просто начните печатать..." + "Just start typing...": "Просто начните печатать...", + "We can't find you, check email or": "Не можем вас найти, проверьте адрес электронной почты или", + "register": "зарегистрируйтесь" } diff --git a/src/pages/author/[slug]/index.astro b/src/pages/author/[slug]/index.astro index 50e29aa4..f7a2cc9b 100644 --- a/src/pages/author/[slug]/index.astro +++ b/src/pages/author/[slug]/index.astro @@ -7,7 +7,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author' const slug = Astro.params.slug.toString() const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT }) -const author = articles[0].authors.find((a) => a.slug === slug) +const author = await apiClient.getAuthor({ slug }) const { pathname, search } = Astro.url initRouter(pathname, search) diff --git a/src/pages/topic/[slug].astro b/src/pages/topic/[slug].astro index 91ebde65..c8f61246 100644 --- a/src/pages/topic/[slug].astro +++ b/src/pages/topic/[slug].astro @@ -6,7 +6,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic' const slug = Astro.params.slug?.toString() || '' const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT }) -const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug) +const topic = await apiClient.getTopic({ slug }) import { initRouter } from '../../stores/router' diff --git a/src/stores/auth.ts b/src/stores/auth.ts index 9662ebe8..b92814fa 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,41 +1,4 @@ -import type { AuthResult } from '../graphql/types.gen' -import { resetToken, setToken } from '../graphql/privateGraphQLClient' import { apiClient } from '../utils/apiClient' -import { createSignal } from 'solid-js' - -const [session, setSession] = createSignal(null) - -export const signIn = async (params) => { - const authResult = await apiClient.authLogin(params) - setSession(authResult) - setToken(authResult.token) - console.debug('signed in') -} -export const signOut = () => { - // TODO: call backend to revoke token - setSession(null) - resetToken() - console.debug('signed out') -} - -export const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({}) - -export const checkEmail = async (email: string): Promise => { - if (emailChecks()[email]) { - return true - } - - const checkResult = await apiClient.authCheckEmail({ email }) - - if (checkResult) { - setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true })) - return true - } - - return false -} - -export const [resetCode, setResetCode] = createSignal('') export const register = async ({ name, @@ -56,19 +19,3 @@ export const register = async ({ export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => { return await apiClient.authSendLink({ email, lang }) } - -export const renewSession = async () => { - const authResult = await apiClient.getSession() // token in header - setToken(authResult.token) - setSession(authResult) -} - -export const confirmEmail = async (token: string) => { - const authResult = await apiClient.confirmEmail({ token }) - setToken(authResult.token) - setSession(authResult) -} - -export const useAuthStore = () => { - return { session, emailChecks } -} diff --git a/src/stores/emailChecks.ts b/src/stores/emailChecks.ts new file mode 100644 index 00000000..62c5e71f --- /dev/null +++ b/src/stores/emailChecks.ts @@ -0,0 +1,23 @@ +import { apiClient } from '../utils/apiClient' +import { createSignal } from 'solid-js' + +const [emailChecks, setEmailChecks] = createSignal<{ [email: string]: boolean }>({}) + +export const checkEmail = async (email: string): Promise => { + if (emailChecks()[email]) { + return true + } + + const checkResult = await apiClient.authCheckEmail({ email }) + + if (checkResult) { + setEmailChecks((oldEmailChecks) => ({ ...oldEmailChecks, [email]: true })) + return true + } + + return false +} + +export const useEmailChecks = () => { + return { emailChecks } +} diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 8b71e4d6..d71801a8 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,6 +1,8 @@ //import { persistentAtom } from '@nanostores/persistent' import { createSignal } from 'solid-js' import { useRouter } from './router' +import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types' +import type { RootSearchParams } from '../components/types' //export const locale = persistentAtom('locale', 'ru') export const [locale, setLocale] = createSignal('ru') @@ -26,10 +28,22 @@ const [modal, setModal] = createSignal(null) const [warnings, setWarnings] = createSignal([]) export const showModal = (modalType: ModalType) => setModal(modalType) + +// TODO: find a better solution export const hideModal = () => { - const { changeSearchParam } = useRouter() + const { searchParams, changeSearchParam } = useRouter< + AuthModalSearchParams & ConfirmEmailSearchParams & RootSearchParams + >() + + if (searchParams().modal === 'auth') { + if (searchParams().mode === 'confirm-email') { + changeSearchParam('token', null, true) + } + changeSearchParam('mode', null, true) + } + changeSearchParam('modal', null, true) - changeSearchParam('mode', null, true) + setModal(null) } diff --git a/src/stores/zine/authors.ts b/src/stores/zine/authors.ts index 2a5cd7f8..527ae5e5 100644 --- a/src/stores/zine/authors.ts +++ b/src/stores/zine/authors.ts @@ -52,9 +52,7 @@ const addAuthors = (authors: Author[]) => { } export const loadAuthor = async ({ slug }: { slug: string }): Promise => { - // TODO: - const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 1 }) - const author = articles[0].authors.find((a) => a.slug === slug) + const author = await apiClient.getAuthor({ slug }) addAuthors([author]) } diff --git a/src/stores/zine/topics.ts b/src/stores/zine/topics.ts index 4992d873..12e3abf3 100644 --- a/src/stores/zine/topics.ts +++ b/src/stores/zine/topics.ts @@ -100,9 +100,7 @@ export const loadRandomTopics = async (): Promise => { } export const loadTopic = async ({ slug }: { slug: string }): Promise => { - // TODO: - const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 1 }) - const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug) + const topic = await apiClient.getTopic({ slug }) addTopics([topic]) } diff --git a/src/styles/AllTopics.module.scss b/src/styles/AllTopics.module.scss index d6aecc23..c146af9e 100644 --- a/src/styles/AllTopics.module.scss +++ b/src/styles/AllTopics.module.scss @@ -35,3 +35,12 @@ .stats { margin-top: 2.4rem; } + +.loadMoreContainer { + margin-top: 48px; + text-align: center; + + .loadMoreButton { + padding: 0.6em 1.5em; + } +} diff --git a/src/styles/app.scss b/src/styles/app.scss index ba11cbcc..7bb2d4b7 100644 --- a/src/styles/app.scss +++ b/src/styles/app.scss @@ -663,7 +663,7 @@ astro-island { width: auto; @include media-breakpoint-down(sm) { - //padding: 0 $container-padding-x * 0.5; + // padding: 0 $container-padding-x * 0.5; } } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index d798488e..61769847 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -1,6 +1,14 @@ -import type { Reaction, Shout, FollowingEntity, AuthResult, ShoutInput } from '../graphql/types.gen' +import type { + Reaction, + Shout, + FollowingEntity, + AuthResult, + ShoutInput, + Topic, + Author +} from '../graphql/types.gen' import { publicGraphQLClient } from '../graphql/publicGraphQLClient' -import { privateGraphQLClient } from '../graphql/privateGraphQLClient' +import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import articleBySlug from '../graphql/query/article-by-slug' import articlesRecentAll from '../graphql/query/articles-recent-all' import articlesRecentPublished from '../graphql/query/articles-recent-published' @@ -25,14 +33,21 @@ import authorsAll from '../graphql/query/authors-all' import reactionCreate from '../graphql/mutation/reaction-create' import reactionDestroy from '../graphql/mutation/reaction-destroy' import reactionUpdate from '../graphql/mutation/reaction-update' -import authorsBySlugs from '../graphql/query/authors-by-slugs' import incrementView from '../graphql/mutation/increment-view' import createArticle from '../graphql/mutation/article-create' import myChats from '../graphql/query/my-chats' +import authorBySlug from '../graphql/query/author-by-slug' +import topicBySlug from '../graphql/query/topic-by-slug' const FEED_SIZE = 50 -type ApiErrorCode = 'unknown' | 'email_not_confirmed' | 'user_not_found' | 'user_already_exists' +type ApiErrorCode = + | 'unknown' + | 'email_not_confirmed' + | 'user_not_found' + | 'user_already_exists' + | 'token_expired' + | 'token_invalid' export class ApiError extends Error { code: ApiErrorCode @@ -44,7 +59,7 @@ export class ApiError extends Error { } export const apiClient = { - authLogin: async ({ email, password }): Promise => { + authLogin: async ({ email, password }: { email: string; password: string }): Promise => { const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise() // console.debug('[api-client] authLogin', { response }) if (response.error) { @@ -98,13 +113,34 @@ export const apiClient = { authSendLink: async ({ email, lang }) => { // send link with code on email const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise() + + if (response.error) { + if (response.error.message === '[GraphQL] User not found') { + throw new ApiError('user_not_found', response.error.message) + } + + throw new ApiError('unknown', response.error.message) + } + + if (response.data.sendLink.error) { + throw new ApiError('unknown', response.data.sendLink.message) + } + return response.data.sendLink }, confirmEmail: async ({ token }: { token: string }) => { // confirm email with code from link const response = await publicGraphQLClient.mutation(authConfirmEmailMutation, { token }).toPromise() - if (response.error) { + // TODO: better error communication + if (response.error.message === '[GraphQL] check token lifetime') { + throw new ApiError('token_expired', response.error.message) + } + + if (response.error.message === '[GraphQL] token is not valid') { + throw new ApiError('token_invalid', response.error.message) + } + throw new ApiError('unknown', response.error.message) } @@ -237,11 +273,14 @@ export const apiClient = { }, getSession: async (): Promise => { + if (!getToken()) { + return null + } + // renew session with auth token in header (!) const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() if (response.error) { - // TODO throw new ApiError('unknown', response.error.message) } @@ -274,9 +313,13 @@ export const apiClient = { } return response.data.authorsAll }, - getAuthor: async ({ slug }: { slug: string }) => { - const response = await publicGraphQLClient.query(authorsBySlugs, { slugs: [slug] }).toPromise() - return response.data.getUsersBySlugs + getAuthor: async ({ slug }: { slug: string }): Promise => { + const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise() + return response.data.getAuthor + }, + getTopic: async ({ slug }: { slug: string }): Promise => { + const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise() + return response.data.getTopic }, getArticle: async ({ slug }: { slug: string }): Promise => { const response = await publicGraphQLClient.query(articleBySlug, { slug }).toPromise() @@ -304,10 +347,6 @@ export const apiClient = { return response.data.reactionsForShouts }, - getAuthorsBySlugs: async ({ slugs }) => { - const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise() - return response.data.getUsersBySlugs - }, createArticle: async ({ article }: { article: ShoutInput }) => { const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise() console.debug('createArticle response:', response)