diff --git a/src/components/Article/Comment.tsx b/src/components/Article/Comment.tsx index ea40a630..17496e02 100644 --- a/src/components/Article/Comment.tsx +++ b/src/components/Article/Comment.tsx @@ -1,5 +1,5 @@ import './Comment.scss' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import { AuthorCard } from '../Author/Card' import { Show, createMemo } from 'solid-js' import { clsx } from 'clsx' diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index ab6d6b07..ecd9f72b 100644 --- a/src/components/Article/FullArticle.tsx +++ b/src/components/Article/FullArticle.tsx @@ -1,16 +1,16 @@ import { capitalize } from '../../utils' import './Full.scss' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import ArticleComment from './Comment' import { AuthorCard } from '../Author/Card' 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..90beb439 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 { Icon } from '../_shared/Icon' 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 35891435..b51d4c13 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -15,10 +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; } diff --git a/src/components/Author/Card.tsx b/src/components/Author/Card.tsx index 4477ee34..7232e583 100644 --- a/src/components/Author/Card.tsx +++ b/src/components/Author/Card.tsx @@ -1,14 +1,14 @@ import type { Author } from '../../graphql/types.gen' import Userpic from './Userpic' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' 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/Discours/Footer.tsx b/src/components/Discours/Footer.tsx index d8df25fb..1f5fe364 100644 --- a/src/components/Discours/Footer.tsx +++ b/src/components/Discours/Footer.tsx @@ -1,6 +1,6 @@ import { createMemo, For } from 'solid-js' import styles from './Footer.module.scss' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import Subscribe from './Subscribe' import { t } from '../../utils/intl' import { locale } from '../../stores/ui' diff --git a/src/components/Feed/Beside.tsx b/src/components/Feed/Beside.tsx index 2a3a7ce2..9ca4533c 100644 --- a/src/components/Feed/Beside.tsx +++ b/src/components/Feed/Beside.tsx @@ -6,7 +6,7 @@ import { AuthorCard } from '../Author/Card' import { TopicCard } from '../Topic/Card' import style from './Beside.module.scss' import type { Author, Shout, Topic, User } from '../../graphql/types.gen' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import { t } from '../../utils/intl' interface BesideProps { diff --git a/src/components/Feed/Card.tsx b/src/components/Feed/Card.tsx index ff431332..5a82a1d9 100644 --- a/src/components/Feed/Card.tsx +++ b/src/components/Feed/Card.tsx @@ -3,7 +3,7 @@ import { createMemo, For, Show } from 'solid-js' import type { Shout } from '../../graphql/types.gen' import { capitalize } from '../../utils' import { translit } from '../../utils/ru2en' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import styles from './Card.module.scss' import { locale } from '../../stores/ui' import { handleClientRouteLinkClick } from '../../stores/router' diff --git a/src/components/Feed/Row2.tsx b/src/components/Feed/Row2.tsx index c009838b..1c0c8b2c 100644 --- a/src/components/Feed/Row2.tsx +++ b/src/components/Feed/Row2.tsx @@ -8,7 +8,7 @@ const x = [ ['8', '4'] ] -export const Row2 = (props: { articles: Shout[] }) => { +export const Row2 = (props: { articles: Shout[]; isEqual?: boolean }) => { const [y, setY] = createSignal(0) createComputed(() => setY(Math.floor(Math.random() * x.length))) @@ -20,8 +20,11 @@ export const Row2 = (props: { articles: Shout[] }) => { {(a, i) => { return ( -
- +
+
) diff --git a/src/components/Feed/Sidebar.tsx b/src/components/Feed/Sidebar.tsx index d930fb78..f9844ad2 100644 --- a/src/components/Feed/Sidebar.tsx +++ b/src/components/Feed/Sidebar.tsx @@ -1,20 +1,20 @@ 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 { Icon } from '../_shared/Icon' import { useTopicsStore } from '../../stores/zine/topics' import { useArticlesStore } from '../../stores/zine/articles' -import { seen } from '../../stores/zine/seen' +import { useSeenStore } from '../../stores/zine/seen' +import { useSession } from '../../context/session' type FeedSidebarProps = { authors: Author[] } export const FeedSidebar = (props: FeedSidebarProps) => { - const getSeen = seen - const { session } = useAuthStore() + const { seen } = useSeenStore() + const { session } = useSession() const { authorEntities } = useAuthorsStore({ authors: props.authors }) const { articlesByTopic } = useArticlesStore() const { topicEntities } = useTopicsStore() diff --git a/src/components/Feed/Slider.scss b/src/components/Feed/Slider.scss index b3131f12..3b739647 100644 --- a/src/components/Feed/Slider.scss +++ b/src/components/Feed/Slider.scss @@ -1,15 +1,18 @@ .swiper-slide { - height: 0 !important; min-height: 0 !important; margin-bottom: 0 !important; - padding-top: 100%; - @include media-breakpoint-up(sm) { - padding-top: 56.2% !important; - } + .cards-with-cover & { + height: 0 !important; + padding-top: 100%; - @include media-breakpoint-up(md) { - padding-top: 35% !important; + @include media-breakpoint-up(sm) { + padding-top: 56.2% !important; + } + + @include media-breakpoint-up(md) { + padding-top: 35% !important; + } } } diff --git a/src/components/Feed/Slider.tsx b/src/components/Feed/Slider.tsx index 87406264..067412d9 100644 --- a/src/components/Feed/Slider.tsx +++ b/src/components/Feed/Slider.tsx @@ -7,11 +7,13 @@ import 'swiper/scss/pagination' import './Slider.scss' import type { Shout } from '../../graphql/types.gen' import { createEffect, createMemo, createSignal, Show, For } from 'solid-js' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' interface SliderProps { title?: string articles: Shout[] + slidesPerView?: number + isCardsWithCover?: boolean } export default (props: SliderProps) => { @@ -19,12 +21,14 @@ export default (props: SliderProps) => { let pagEl: HTMLDivElement | undefined let nextEl: HTMLDivElement | undefined let prevEl: HTMLDivElement | undefined + + const isCardsWithCover = typeof props.isCardsWithCover === 'boolean' ? props.isCardsWithCover : true + const opts: SwiperOptions = { roundLengths: true, loop: true, centeredSlides: true, slidesPerView: 1, - spaceBetween: 8, modules: [Navigation, Pagination], speed: 500, navigation: { nextEl, prevEl }, @@ -35,7 +39,12 @@ export default (props: SliderProps) => { }, breakpoints: { 768: { - slidesPerView: 1.66666 + slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666, + spaceBetween: isCardsWithCover ? 8 : 26 + }, + 992: { + slidesPerView: props.slidesPerView > 0 ? props.slidesPerView : 1.66666, + spaceBetween: isCardsWithCover ? 8 : 52 } } } @@ -49,12 +58,13 @@ export default (props: SliderProps) => { } }) const articles = createMemo(() => props.articles) + return (

{props.title}

-
+
{(a: Shout) => ( @@ -63,7 +73,7 @@ export default (props: SliderProps) => { settings={{ additionalClass: 'swiper-slide', isFloorImportant: true, - isWithCover: true, + isWithCover: isCardsWithCover, nodate: true }} /> @@ -71,10 +81,10 @@ export default (props: SliderProps) => {
swiper()?.slideNext()}> - +
swiper()?.slidePrev()}> - +
diff --git a/src/components/Inbox/Search.tsx b/src/components/Inbox/Search.tsx index d180331a..90ce7423 100644 --- a/src/components/Inbox/Search.tsx +++ b/src/components/Inbox/Search.tsx @@ -1,6 +1,6 @@ import styles from './Search.module.scss' import { createSignal } from 'solid-js' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' type Props = { placeholder: string 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 0dc12583..bfe024ae 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,10 +54,12 @@ export const ForgotPasswordForm = () => { setIsSubmitting(true) try { - const result = await signSendLink({ email: email(), lang: locale() }) - if (result.error) setSubmitError(result.error) - else setSended(true) + 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) @@ -77,6 +82,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/SocialProviders.tsx b/src/components/Nav/AuthModal/SocialProviders.tsx index 1daefd67..fcbe1309 100644 --- a/src/components/Nav/AuthModal/SocialProviders.tsx +++ b/src/components/Nav/AuthModal/SocialProviders.tsx @@ -1,5 +1,5 @@ import { t } from '../../../utils/intl' -import { Icon } from '../Icon' +import { Icon } from '../../_shared/Icon' import { hideModal } from '../../../stores/ui' import styles from './SocialProviders.module.scss' diff --git a/src/components/Nav/AuthModal/index.tsx b/src/components/Nav/AuthModal/index.tsx index f0a773d1..9890dce0 100644 --- a/src/components/Nav/AuthModal/index.tsx +++ b/src/components/Nav/AuthModal/index.tsx @@ -39,7 +39,7 @@ export const AuthModal = () => { class={clsx('row', styles.view)} classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }} > -
+
{

-
+
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.module.scss b/src/components/Nav/Header.module.scss index 74a81550..f683397b 100644 --- a/src/components/Nav/Header.module.scss +++ b/src/components/Nav/Header.module.scss @@ -71,12 +71,16 @@ align-items: center; display: inline-flex; height: 56px; - padding: 0 $container-padding-x 0 0; + padding: 0; position: relative; transition: height 0.2s; text-align: center; z-index: 9; + @include media-breakpoint-up(sm) { + padding: 0 6rem 0 0; + } + @include media-breakpoint-up(md) { height: 70px; } @@ -114,6 +118,7 @@ .usernav { display: inline-flex; + font-weight: 500; padding-right: 0; position: relative; width: auto; @@ -141,6 +146,7 @@ .mainNavigation { display: inline-flex; + font-weight: 500; list-style: none; margin: 0; opacity: 1; @@ -204,10 +210,14 @@ box-sizing: content-box; display: inline-flex; float: right; + padding-left: 0; padding-right: 0; - padding-left: divide($container-padding-x, 2); width: 2.2rem; + @include media-breakpoint-up(sm) { + padding-left: divide($container-padding-x, 2); + } + @include media-breakpoint-up(md) { display: none; } @@ -318,12 +328,16 @@ .articleControls { display: flex; justify-content: flex-end; - left: 0; position: absolute; + right: 2rem; top: 50%; transform: translateY(-50%); width: 100%; + @include media-breakpoint-up(md) { + right: 0; + } + .control { cursor: pointer; border: 0; @@ -344,7 +358,11 @@ } .control + .control { - margin-left: 1.6rem; + margin-left: 1.2rem; + + @include media-breakpoint-up(sm) { + margin-left: 2rem; + } } img { @@ -390,12 +408,20 @@ display: flex; height: 2.4em; justify-content: center; - margin-left: divide($container-padding-x, 2); + margin-left: divide($container-padding-x, 4); position: relative; + transition: margin-left 0.3s; width: 2.4em; - @include media-breakpoint-up(sm) { - margin-left: 1.2rem; + @include media-breakpoint-down(sm) { + margin-left: 0.4rem !important; + } + + .headerScrolledTop &, + .headerScrolledBottom & { + border-color: transparent; + margin-left: 0; + transition: none; } .circlewrap { @@ -459,7 +485,7 @@ } } -.userControlItemWritePost { +.userControlItemVerbose { @include media-breakpoint-up(lg) { width: auto; diff --git a/src/components/Nav/Header.tsx b/src/components/Nav/Header.tsx index f87c6825..a21b78e6 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 { Icon } from './Icon' +import { For, Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js' +import { Icon } from '../_shared/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,85 +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..9f838fec --- /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 '../_shared/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..f79526e0 100644 --- a/src/components/Nav/Topics.tsx +++ b/src/components/Nav/Topics.tsx @@ -1,9 +1,10 @@ import { For, Show } from 'solid-js' import type { Topic } from '../../graphql/types.gen' -import { Icon } from './Icon' +import { Icon } from '../_shared/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/AllAuthorsPage.tsx b/src/components/Pages/AllAuthorsPage.tsx index 16c710d3..01546c1b 100644 --- a/src/components/Pages/AllAuthorsPage.tsx +++ b/src/components/Pages/AllAuthorsPage.tsx @@ -1,27 +1,14 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { AllAuthorsView } from '../Views/AllAuthors' import type { PageProps } from '../types' -import { createSignal, onMount, Show } from 'solid-js' -import { loadAllAuthors } from '../../stores/zine/authors' -import { Loading } from '../Loading' +import { ClientContainer } from '../_shared/ClientContainer' export const AllAuthorsPage = (props: PageProps) => { - const [isLoaded, setIsLoaded] = createSignal(Boolean(props.allAuthors)) - - onMount(async () => { - if (isLoaded()) { - return - } - - await loadAllAuthors() - setIsLoaded(true) - }) - return ( - }> + - + ) } diff --git a/src/components/Pages/AllTopicsPage.tsx b/src/components/Pages/AllTopicsPage.tsx index d188771a..f030543a 100644 --- a/src/components/Pages/AllTopicsPage.tsx +++ b/src/components/Pages/AllTopicsPage.tsx @@ -1,27 +1,14 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { AllTopicsView } from '../Views/AllTopics' import type { PageProps } from '../types' -import { createSignal, onMount, Show } from 'solid-js' -import { loadAllTopics } from '../../stores/zine/topics' -import { Loading } from '../Loading' +import { ClientContainer } from '../_shared/ClientContainer' export const AllTopicsPage = (props: PageProps) => { - const [isLoaded, setIsLoaded] = createSignal(Boolean(props.allTopics)) - - onMount(async () => { - if (isLoaded()) { - return - } - - await loadAllTopics() - setIsLoaded(true) - }) - return ( - }> + - + ) } diff --git a/src/components/Pages/ArticlePage.tsx b/src/components/Pages/ArticlePage.tsx index 4a465b03..e7877680 100644 --- a/src/components/Pages/ArticlePage.tsx +++ b/src/components/Pages/ArticlePage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { ArticleView } from '../Views/Article' import type { PageProps } from '../types' import { loadArticle, useArticlesStore } from '../../stores/zine/articles' diff --git a/src/components/Pages/AuthorPage.tsx b/src/components/Pages/AuthorPage.tsx index 467add97..d8f7f755 100644 --- a/src/components/Pages/AuthorPage.tsx +++ b/src/components/Pages/AuthorPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../Views/Author' import type { PageProps } from '../types' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' diff --git a/src/components/Pages/ConnectPage.tsx b/src/components/Pages/ConnectPage.tsx index ed722e12..ca494ef0 100644 --- a/src/components/Pages/ConnectPage.tsx +++ b/src/components/Pages/ConnectPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' export const ConnectPage = () => { return ( diff --git a/src/components/Pages/CreatePage.tsx b/src/components/Pages/CreatePage.tsx index 60fa4255..f800769e 100644 --- a/src/components/Pages/CreatePage.tsx +++ b/src/components/Pages/CreatePage.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense } from 'solid-js' -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { Loading } from '../Loading' const CreateView = lazy(() => import('../Views/Create')) diff --git a/src/components/Pages/FeedPage.tsx b/src/components/Pages/FeedPage.tsx index 8067859e..dc7dc3ac 100644 --- a/src/components/Pages/FeedPage.tsx +++ b/src/components/Pages/FeedPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { FeedView } from '../Views/Feed' import { onCleanup } from 'solid-js' import { resetSortedArticles } from '../../stores/zine/articles' diff --git a/src/components/Pages/FourOuFourPage.tsx b/src/components/Pages/FourOuFourPage.tsx index 31137f13..86bd076a 100644 --- a/src/components/Pages/FourOuFourPage.tsx +++ b/src/components/Pages/FourOuFourPage.tsx @@ -1,5 +1,5 @@ import { FourOuFourView } from '../Views/FourOuFour' -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' export const FourOuFourPage = () => { return ( diff --git a/src/components/Pages/HomePage.tsx b/src/components/Pages/HomePage.tsx index 04cb93d0..f29a8111 100644 --- a/src/components/Pages/HomePage.tsx +++ b/src/components/Pages/HomePage.tsx @@ -1,5 +1,5 @@ import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../Views/Home' -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import type { PageProps } from '../types' import { createSignal, onCleanup, onMount, Show } from 'solid-js' import { loadPublishedArticles, resetSortedArticles } from '../../stores/zine/articles' diff --git a/src/components/Pages/LayoutShoutsPage.tsx b/src/components/Pages/LayoutShoutsPage.tsx index 7f0282b2..d2eabce8 100644 --- a/src/components/Pages/LayoutShoutsPage.tsx +++ b/src/components/Pages/LayoutShoutsPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import type { PageProps } from '../types' import { createMemo, createSignal, For, onCleanup, onMount, Show } from 'solid-js' import { resetSortedArticles } from '../../stores/zine/articles' diff --git a/src/components/Pages/SearchPage.tsx b/src/components/Pages/SearchPage.tsx index 497d109c..84d3547d 100644 --- a/src/components/Pages/SearchPage.tsx +++ b/src/components/Pages/SearchPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { SearchView } from '../Views/Search' import type { PageProps } from '../types' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' diff --git a/src/components/Pages/TopicPage.tsx b/src/components/Pages/TopicPage.tsx index ad4fdc1f..289dabd9 100644 --- a/src/components/Pages/TopicPage.tsx +++ b/src/components/Pages/TopicPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../Wraps/PageWrap' +import { PageWrap } from '../_shared/PageWrap' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic' import type { PageProps } from '../types' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' @@ -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.shouts) && Boolean(props.author)) + const [isLoaded, setIsLoaded] = createSignal(Boolean(props.shouts) && Boolean(props.topic)) const slug = createMemo(() => { const { page: getPage } = useRouter() diff --git a/src/components/Pages/about/DiscussionRulesPage.tsx b/src/components/Pages/about/DiscussionRulesPage.tsx index 735c2389..935639bc 100644 --- a/src/components/Pages/about/DiscussionRulesPage.tsx +++ b/src/components/Pages/about/DiscussionRulesPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { t } from '../../../utils/intl' export const DiscussionRulesPage = () => { diff --git a/src/components/Pages/about/DogmaPage.tsx b/src/components/Pages/about/DogmaPage.tsx index ff43de62..1dcba449 100644 --- a/src/components/Pages/about/DogmaPage.tsx +++ b/src/components/Pages/about/DogmaPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' // const title = t('Dogma') diff --git a/src/components/Pages/about/GuidePage.tsx b/src/components/Pages/about/GuidePage.tsx index 99ace509..17213e64 100644 --- a/src/components/Pages/about/GuidePage.tsx +++ b/src/components/Pages/about/GuidePage.tsx @@ -1,7 +1,7 @@ import { createSignal, Show } from 'solid-js' -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { t } from '../../../utils/intl' -import { Icon } from '../../Nav/Icon' +import { Icon } from '../../_shared/Icon' export const GuidePage = () => { const title = t('How it works') diff --git a/src/components/Pages/about/HelpPage.tsx b/src/components/Pages/about/HelpPage.tsx index 5b725b1f..f5be324b 100644 --- a/src/components/Pages/about/HelpPage.tsx +++ b/src/components/Pages/about/HelpPage.tsx @@ -1,7 +1,7 @@ import { createSignal, Show } from 'solid-js' -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { Donate } from '../../Discours/Donate' -import { Icon } from '../../Nav/Icon' +import { Icon } from '../../_shared/Icon' // const title = t('Support us') diff --git a/src/components/Pages/about/ManifestPage.tsx b/src/components/Pages/about/ManifestPage.tsx index 32b48ec0..79d1b9c0 100644 --- a/src/components/Pages/about/ManifestPage.tsx +++ b/src/components/Pages/about/ManifestPage.tsx @@ -1,10 +1,10 @@ import { createSignal, Show } from 'solid-js' -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { Modal } from '../../Nav/Modal' import { Feedback } from '../../Discours/Feedback' import Subscribe from '../../Discours/Subscribe' import Opener from '../../Nav/Opener' -import { Icon } from '../../Nav/Icon' +import { Icon } from '../../_shared/Icon' // title={t('Manifest')} diff --git a/src/components/Pages/about/PartnersPage.tsx b/src/components/Pages/about/PartnersPage.tsx index 35a2ab29..e01aadac 100644 --- a/src/components/Pages/about/PartnersPage.tsx +++ b/src/components/Pages/about/PartnersPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { t } from '../../../utils/intl' // const title = t('Partners') diff --git a/src/components/Pages/about/PrinciplesPage.tsx b/src/components/Pages/about/PrinciplesPage.tsx index 60abeae6..3d6f0ab2 100644 --- a/src/components/Pages/about/PrinciplesPage.tsx +++ b/src/components/Pages/about/PrinciplesPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { t } from '../../../utils/intl' export const PrinciplesPage = () => { diff --git a/src/components/Pages/about/ProjectsPage.tsx b/src/components/Pages/about/ProjectsPage.tsx index f051d55d..63b52fc7 100644 --- a/src/components/Pages/about/ProjectsPage.tsx +++ b/src/components/Pages/about/ProjectsPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { t } from '../../../utils/intl' // title={t('Projects')}> diff --git a/src/components/Pages/about/TermsOfUsePage.tsx b/src/components/Pages/about/TermsOfUsePage.tsx index 6f395bad..a2a0e5cf 100644 --- a/src/components/Pages/about/TermsOfUsePage.tsx +++ b/src/components/Pages/about/TermsOfUsePage.tsx @@ -1,6 +1,6 @@ import { createSignal, Show } from 'solid-js' -import { PageWrap } from '../../Wraps/PageWrap' -import { Icon } from '../../Nav/Icon' +import { PageWrap } from '../../_shared/PageWrap' +import { Icon } from '../../_shared/Icon' // const title = t('Terms of use') diff --git a/src/components/Pages/about/ThanksPage.tsx b/src/components/Pages/about/ThanksPage.tsx index e1998c70..23af76be 100644 --- a/src/components/Pages/about/ThanksPage.tsx +++ b/src/components/Pages/about/ThanksPage.tsx @@ -1,4 +1,4 @@ -import { PageWrap } from '../../Wraps/PageWrap' +import { PageWrap } from '../../_shared/PageWrap' import { t } from '../../../utils/intl' export const ThanksPage = () => { diff --git a/src/components/Root.tsx b/src/components/Root.tsx index ade7d784..fc9955d7 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,19 +29,12 @@ 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 { LayoutShoutsPage } from './Pages/LayoutShoutsPage' +import { SessionProvider } from '../context/session' // TODO: lazy load // const SomePage = lazy(() => import('./Pages/SomePage')) -const log = getLogger('root') - -type RootSearchParams = { - modal: string - lang: string -} - const pagesMap: Record> = { expo: LayoutShoutsPage, connect: ConnectPage, @@ -77,10 +69,6 @@ export const Root = (props: PageProps) => { } }) - onMount(() => { - renewSession() - }) - const pageComponent = createMemo(() => { const result = pagesMap[page().route] @@ -99,5 +87,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/FloorHeader.tsx b/src/components/Topic/FloorHeader.tsx index 9c122794..398197c8 100644 --- a/src/components/Topic/FloorHeader.tsx +++ b/src/components/Topic/FloorHeader.tsx @@ -1,5 +1,5 @@ import type { Topic } from '../../graphql/types.gen' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import './FloorHeader.scss' import { t } from '../../utils/intl' 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..7bae0ad0 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 { Icon } from '../_shared/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..b64bad6b 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 { Icon } from '../_shared/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..bb7b7fe8 100644 --- a/src/components/Views/Feed.tsx +++ b/src/components/Views/Feed.tsx @@ -1,20 +1,20 @@ import { createMemo, createSignal, For, onMount, Show } from 'solid-js' import '../../styles/Feed.scss' import stylesBeside from '../../components/Feed/Beside.module.scss' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import { byCreated, sortBy } from '../../utils/sortby' import { TopicCard } from '../Topic/Card' 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/FourOuFour.tsx b/src/components/Views/FourOuFour.tsx index add7879a..bd9974fc 100644 --- a/src/components/Views/FourOuFour.tsx +++ b/src/components/Views/FourOuFour.tsx @@ -1,5 +1,5 @@ import { t } from '../../utils/intl' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import styles from '../../styles/FourOuFour.module.scss' import { clsx } from 'clsx' diff --git a/src/components/Views/Home.tsx b/src/components/Views/Home.tsx index d1246a86..d8dd693f 100644 --- a/src/components/Views/Home.tsx +++ b/src/components/Views/Home.tsx @@ -11,7 +11,7 @@ import RowShort from '../Feed/RowShort' import Slider from '../Feed/Slider' import Group from '../Feed/Group' import type { Shout, Topic } from '../../graphql/types.gen' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import { t } from '../../utils/intl' import { useTopicsStore } from '../../stores/zine/topics' import { diff --git a/src/components/Views/Inbox.tsx b/src/components/Views/Inbox.tsx index 19f5662d..0f8bea76 100644 --- a/src/components/Views/Inbox.tsx +++ b/src/components/Views/Inbox.tsx @@ -1,7 +1,7 @@ import { For, createSignal, Show, onMount, createEffect } from 'solid-js' import type { Author } from '../../graphql/types.gen' import { AuthorCard } from '../Author/Card' -import { Icon } from '../Nav/Icon' +import { Icon } from '../_shared/Icon' import { Loading } from '../Loading' import DialogCard from '../Inbox/DialogCard' import Search from '../Inbox/Search' diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index a2760967..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' @@ -26,7 +26,7 @@ interface TopicProps { topicSlug: string } -export const PRERENDERED_ARTICLES_COUNT = 21 +export const PRERENDERED_ARTICLES_COUNT = 28 const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3 export const TopicView = (props: TopicProps) => { @@ -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 }) @@ -112,7 +113,7 @@ 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/Icon.css b/src/components/_shared/Icon.module.scss similarity index 51% rename from src/components/Nav/Icon.css rename to src/components/_shared/Icon.module.scss index c59128ed..64609e56 100644 --- a/src/components/Nav/Icon.css +++ b/src/components/_shared/Icon.module.scss @@ -8,17 +8,19 @@ img { height: 100%; } -.notifications-counter { - background-color: red; - border-radius: 1rem; +.notificationsCounter { + background-color: #d00820; + border: 2px solid #fff; + border-radius: 2em; color: #fff; font-size: 1rem; font-weight: 700; - height: 1.5em; - line-height: 1.5em; + height: 1.6em; + left: 1.1em; + line-height: 1.25em; + padding: 0 0.25em; position: absolute; - right: -0.5rem; text-align: center; top: -0.5rem; - width: 1.5em; + min-width: 1.5em; } diff --git a/src/components/Nav/Icon.tsx b/src/components/_shared/Icon.tsx similarity index 74% rename from src/components/Nav/Icon.tsx rename to src/components/_shared/Icon.tsx index 227e2115..19652b9d 100644 --- a/src/components/Nav/Icon.tsx +++ b/src/components/_shared/Icon.tsx @@ -1,7 +1,7 @@ import { mergeProps, Show } from 'solid-js' import type { JSX } from 'solid-js' import { clsx } from 'clsx' -import './Icon.css' +import styles from './Icon.module.scss' type IconProps = { class?: string @@ -16,10 +16,10 @@ export const Icon = (passedProps: IconProps) => { const props = mergeProps({ title: '', counter: 0 }, passedProps) return ( -
    +
    {props.title -
    {props.counter}
    +
    {props.counter}
    ) diff --git a/src/components/Wraps/PageWrap.tsx b/src/components/_shared/PageWrap.tsx similarity index 100% rename from src/components/Wraps/PageWrap.tsx rename to src/components/_shared/PageWrap.tsx 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 d161461d..b0d5489a 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/locales/ru.json b/src/locales/ru.json index 8f85bb84..568e555b 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -173,5 +173,7 @@ "Artworks": "Артворки", "Audio": "Аудио", "Video": "Видео", - "Literature": "Литература" + "Literature": "Литература", + "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 f43de677..7316a87f 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 shouts = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT }) -const author = shouts[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 13ba2318..491c30fa 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 shouts = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT }) -const topic = shouts[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 5b07ad25..b92814fa 100644 --- a/src/stores/auth.ts +++ b/src/stores/auth.ts @@ -1,45 +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 = async () => { - const result = await apiClient.authSignOut() - if (result.error) { - console.error('[auth] sign out error', result.error) - } else { - 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, @@ -60,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 b9f6ef31..cbff3f54 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -1,5 +1,7 @@ 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, setLocale] = createSignal('ru') export type ModalType = 'auth' | 'subscribe' | 'feedback' | 'thank' | 'donate' @@ -24,10 +26,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/seen.ts b/src/stores/zine/seen.ts index 5f2c4c14..fdc3fe78 100644 --- a/src/stores/zine/seen.ts +++ b/src/stores/zine/seen.ts @@ -3,3 +3,13 @@ import { createStorageSignal } from '@solid-primitives/storage' // local stored seen marks by shout's slug export const [seen, setSeen] = createStorageSignal<{ [slug: string]: Date }>('seen', {}) export const addSeen = (slug) => setSeen({ ...seen(), [slug]: Date.now() }) + +export const useSeenStore = (initialData: { [slug: string]: Date } = {}) => { + setSeen({ ...seen(), ...initialData }) + + return { + seen, + setSeen, + addSeen + } +} 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 18ef91a6..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; } } @@ -690,11 +690,11 @@ astro-island { .shift-content { @include media-breakpoint-up(md) { - margin-left: 127px; + margin-left: 161px; } @include media-breakpoint-up(lg) { - margin-left: 201px; + margin-left: 235px; } } diff --git a/src/utils/apiClient.ts b/src/utils/apiClient.ts index 37aed542..15f77fd8 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,7 +33,6 @@ 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/im-chats' @@ -34,10 +41,18 @@ import getRecentByLayout from '../graphql/query/layout-recent' import getTopByLayout from '../graphql/query/layout-top' import getTopMonthByLayout from '../graphql/query/layout-top-month' import type { LayoutType } from '../stores/zine/layouts' +import topicBySlug from '../graphql/query/topic-by-slug' +import authorBySlug from '../graphql/query/author-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 @@ -49,7 +64,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) { @@ -103,13 +118,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) } @@ -244,11 +280,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) } @@ -281,9 +320,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() @@ -311,10 +354,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) diff --git a/src/utils/config.ts b/src/utils/config.ts index bb6d362c..8ce302e4 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,2 +1,2 @@ export const isDev = import.meta.env.VERCEL_ENV !== 'production' -export const apiBaseUrl = import.meta.env.API_URL +export const apiBaseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'