diff --git a/codegen.yml b/codegen.yml index 1e8c8a47..04ea8542 100644 --- a/codegen.yml +++ b/codegen.yml @@ -1,5 +1,5 @@ overwrite: true -schema: 'https://newapi.discours.io/graphql' +schema: 'https://testapi.discours.io/graphql' generates: src/graphql/introspec.gen.ts: plugins: diff --git a/src/components/Article/FullArticle.tsx b/src/components/Article/FullArticle.tsx index ab6d6b07..717aff64 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 { useAuth } from '../../context/auth' const MAX_COMMENT_LEVEL = 6 @@ -38,7 +38,7 @@ const formatDate = (date: Date) => { } export const FullArticle = (props: ArticleProps) => { - const { session } = useAuthStore() + const { session } = useAuth() onMount(() => { incrementView({ articleSlug: props.article.slug }) diff --git a/src/components/Author/Card.module.scss b/src/components/Author/Card.module.scss index ad75417e..bb45951c 100644 --- a/src/components/Author/Card.module.scss +++ b/src/components/Author/Card.module.scss @@ -15,7 +15,7 @@ .authorDetails { display: flex; flex: 1; - //padding-right: 1.2rem; + // padding-right: 1.2rem; width: max-content; @include media-breakpoint-down(sm) { @@ -242,6 +242,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..7cfa677d 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 { useAuth } from '../../context/auth' interface AuthorCardProps { compact?: boolean @@ -23,7 +23,7 @@ interface AuthorCardProps { } export const AuthorCard = (props: AuthorCardProps) => { - const { session } = useAuthStore() + const { session } = useAuth() const subscribed = createMemo( () => session()?.news?.authors?.some((u) => u === props.author.slug) || false 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 09dac4a8..31b8858c 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 { useAuth } from '../../context/auth' type FeedSidebarProps = { authors: Author[] @@ -14,7 +14,7 @@ type FeedSidebarProps = { export const FeedSidebar = (props: FeedSidebarProps) => { const { getSeen: seen } = useSeenStore() - const { session } = useAuthStore() + const { session } = useAuth() 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 97529b77..b398f78d 100644 --- a/src/components/Feed/Slider.tsx +++ b/src/components/Feed/Slider.tsx @@ -12,6 +12,8 @@ import { Icon } from '../Nav/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 }} /> diff --git a/src/components/Nav/AuthModal/EmailConfirm.tsx b/src/components/Nav/AuthModal/EmailConfirm.tsx index 1fd1c7aa..d458ca3b 100644 --- a/src/components/Nav/AuthModal/EmailConfirm.tsx +++ b/src/components/Nav/AuthModal/EmailConfirm.tsx @@ -4,14 +4,14 @@ 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 type { ConfirmEmailSearchParams } from './types' +import { useAuth } from '../../../context/auth' export const EmailConfirm = () => { - const { session } = useAuthStore() + const { + session, + actions: { confirmEmail } + } = useAuth() const confirmedEmail = createMemo(() => session()?.user?.email || '') diff --git a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx index f19a89f4..d6b3621e 100644 --- a/src/components/Nav/AuthModal/ForgotPasswordForm.tsx +++ b/src/components/Nav/AuthModal/ForgotPasswordForm.tsx @@ -6,8 +6,8 @@ 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 { signSendLink } from '../../../context/auth' type FormFields = { email: string diff --git a/src/components/Nav/AuthModal/LoginForm.tsx b/src/components/Nav/AuthModal/LoginForm.tsx index f098f392..92d5cfd0 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,7 @@ import { email, setEmail } from './sharedLogic' import { useRouter } from '../../../stores/router' import type { AuthModalSearchParams } from './types' import { hideModal, locale } from '../../../stores/ui' +import { signSendLink, useAuth } from '../../../context/auth' type FormFields = { email: string @@ -26,6 +26,10 @@ export const LoginForm = () => { const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) const [isLinkSent, setIsLinkSent] = createSignal(false) + const { + actions: { signIn } + } = useAuth() + const { changeSearchParam } = useRouter() const [password, setPassword] = createSignal('') diff --git a/src/components/Nav/AuthModal/RegisterForm.tsx b/src/components/Nav/AuthModal/RegisterForm.tsx index 3e464b05..d936f659 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 '../../../context/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..970d9005 --- /dev/null +++ b/src/components/Nav/HeaderAuth.tsx @@ -0,0 +1,111 @@ +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 { useAuth } from '../../context/auth' + +type HeaderAuthProps = { + setIsProfilePopupVisible: (value: boolean) => void +} + +export const HeaderAuth = (props: HeaderAuthProps) => { + const [isMounted, setIsMounted] = createSignal(false) + const { page } = useRouter() + const [visibleWarnings, setVisibleWarnings] = createSignal(false) + const { warnings } = useWarningsStore() + + const { session, isAuthenticated } = useAuth() + + const toggleWarnings = () => setVisibleWarnings(!visibleWarnings()) + + const handleBellIconClick = (event: Event) => { + event.preventDefault() + + if (!isAuthenticated()) { + showModal('auth') + return + } + + toggleWarnings() + } + + onMount(() => { + setIsMounted(true) + }) + + return ( + + +
+ + } + > + + { + props.setIsProfilePopupVisible(isVisible) + }} + containerCssClass={styles.control} + trigger={ +
+ +
+ } + /> + +
+
+ + + ) +} diff --git a/src/components/Nav/ProfileModal.tsx b/src/components/Nav/ProfileModal.tsx index 2beab06f..10b69d7f 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 { useAuth } from '../../context/auth' -const quit = () => { - signOut() - hideModal() -} +export const ProfileModal = () => { + const { + session, + actions: { signOut } + } = useAuth() -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..f0fb7772 100644 --- a/src/components/Nav/ProfilePopup.tsx +++ b/src/components/Nav/ProfilePopup.tsx @@ -1,11 +1,14 @@ import { Popup, PopupProps } from './Popup' -import { signOut, useAuthStore } from '../../stores/auth' import styles from './Popup.module.scss' +import { useAuth } from '../../context/auth' type ProfilePopupProps = Omit export const ProfilePopup = (props: ProfilePopupProps) => { - const { session } = useAuthStore() + const { + session, + actions: { signOut } + } = useAuth() 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..787bcca1 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 { AuthProvider } from '../context/auth' // 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..ffcf8b9f 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 { useAuth } from '../../context/auth' const log = getLogger('TopicCard') @@ -24,7 +24,7 @@ interface TopicProps { } export const TopicCard = (props: TopicProps) => { - const { session } = useAuthStore() + const { session } = useAuth() 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..70d78da8 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 { useAuth } from '../../context/auth' type Props = { topic: Topic } export const FullTopic = (props: Props) => { - const { session } = useAuthStore() + const { session } = useAuth() 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..eec96c19 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 { useAuth } from '../../context/auth' 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 } = useAuth() 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..2261a10c 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 { useAuth } from '../../context/auth' 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 } = useAuth() 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/Feed.tsx b/src/components/Views/Feed.tsx index c791331c..36318f12 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 { useAuth } from '../../context/auth' // 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 } = useAuth() const topReactions = createMemo(() => sortBy(reactions(), byCreated)) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index a2760967..27c0d27e 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/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/auth.tsx b/src/context/auth.tsx new file mode 100644 index 00000000..7fe11945 --- /dev/null +++ b/src/context/auth.tsx @@ -0,0 +1,98 @@ +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 AuthContextType = { + session: InitializedResource + isAuthenticated: Accessor + actions: { + refreshSession: () => AuthResult | Promise + signIn: ({ email, password }: { email: string; password: string }) => Promise + signOut: () => Promise + confirmEmail: (token: string) => Promise + } +} + +const AuthContext = createContext() + +const refreshSession = async (): Promise => { + try { + const authResult = await apiClient.getSession() + setToken(authResult.token) + return authResult + } catch (error) { + console.error('renewSession error:', error) + resetToken() + return null + } +} + +export const register = async ({ + name, + email, + password +}: { + name: string + email: string + password: string +}) => { + await apiClient.authRegister({ + name, + email, + password + }) +} + +export const signSendLink = async ({ email, lang }: { email: string; lang: string }) => { + return await apiClient.authSendLink({ email, lang }) +} + +export function useAuth() { + return useContext(AuthContext) +} + +export const AuthProvider = (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: AuthContextType = { session, isAuthenticated, actions } + + onMount(() => { + refetchRefreshSession() + }) + + return {props.children} +} diff --git a/src/graphql/privateGraphQLClient.ts b/src/graphql/privateGraphQLClient.ts index dc9ff986..0de1202a 100644 --- a/src/graphql/privateGraphQLClient.ts +++ b/src/graphql/privateGraphQLClient.ts @@ -27,7 +27,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/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 deleted file mode 100644 index 9662ebe8..00000000 --- a/src/stores/auth.ts +++ /dev/null @@ -1,74 +0,0 @@ -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, - email, - password -}: { - name: string - email: string - password: string -}) => { - await apiClient.authRegister({ - name, - email, - password - }) -} - -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..8f8e65eb 100644 --- a/src/utils/apiClient.ts +++ b/src/utils/apiClient.ts @@ -1,4 +1,12 @@ -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 articleBySlug from '../graphql/query/article-by-slug' @@ -25,10 +33,11 @@ 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 @@ -44,7 +53,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,6 +107,11 @@ 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) { + throw new ApiError('unknown', response.error.message) + } + return response.data.sendLink }, confirmEmail: async ({ token }: { token: string }) => { @@ -241,7 +255,6 @@ export const apiClient = { const response = await privateGraphQLClient.mutation(mySession, {}).toPromise() if (response.error) { - // TODO throw new ApiError('unknown', response.error.message) } @@ -274,9 +287,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 +321,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 ab0ab70b..4678a034 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,4 +1,4 @@ export const isDev = import.meta.env.MODE === 'development' -export const apiBaseUrl = 'https://newapi.discours.io' +export const apiBaseUrl = 'https://testapi.discours.io' // export const apiBaseUrl = 'http://localhost:8080'