auth context
topic page fix, getAuthor, getTopic newapi -> testapi fixes
This commit is contained in:
parent
fd15d04231
commit
11874c6c1d
|
@ -1,5 +1,5 @@
|
||||||
overwrite: true
|
overwrite: true
|
||||||
schema: 'https://newapi.discours.io/graphql'
|
schema: 'https://testapi.discours.io/graphql'
|
||||||
generates:
|
generates:
|
||||||
src/graphql/introspec.gen.ts:
|
src/graphql/introspec.gen.ts:
|
||||||
plugins:
|
plugins:
|
||||||
|
|
|
@ -7,10 +7,10 @@ import { createMemo, For, onMount, Show } from 'solid-js'
|
||||||
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
import type { Author, Reaction, Shout } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { showModal } from '../../stores/ui'
|
import { showModal } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { incrementView } from '../../stores/zine/articles'
|
import { incrementView } from '../../stores/zine/articles'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
import { SharePopup } from './SharePopup'
|
import { SharePopup } from './SharePopup'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
const MAX_COMMENT_LEVEL = 6
|
const MAX_COMMENT_LEVEL = 6
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ const formatDate = (date: Date) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullArticle = (props: ArticleProps) => {
|
export const FullArticle = (props: ArticleProps) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
incrementView({ articleSlug: props.article.slug })
|
incrementView({ articleSlug: props.article.slug })
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
.authorDetails {
|
.authorDetails {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
//padding-right: 1.2rem;
|
// padding-right: 1.2rem;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
|
@ -242,6 +242,7 @@
|
||||||
.authorsListItem {
|
.authorsListItem {
|
||||||
.authorName {
|
.authorName {
|
||||||
@include font-size(2.2rem);
|
@include font-size(2.2rem);
|
||||||
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,10 +5,10 @@ import styles from './Card.module.scss'
|
||||||
import { createMemo, For, Show } from 'solid-js'
|
import { createMemo, For, Show } from 'solid-js'
|
||||||
import { translit } from '../../utils/ru2en'
|
import { translit } from '../../utils/ru2en'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
interface AuthorCardProps {
|
interface AuthorCardProps {
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
|
@ -23,7 +23,7 @@ interface AuthorCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorCard = (props: AuthorCardProps) => {
|
export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
const subscribed = createMemo<boolean>(
|
const subscribed = createMemo<boolean>(
|
||||||
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
() => session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||||
|
|
|
@ -8,7 +8,7 @@ const x = [
|
||||||
['8', '4']
|
['8', '4']
|
||||||
]
|
]
|
||||||
|
|
||||||
export const Row2 = (props: { articles: Shout[] }) => {
|
export const Row2 = (props: { articles: Shout[]; isEqual?: boolean }) => {
|
||||||
const [y, setY] = createSignal(0)
|
const [y, setY] = createSignal(0)
|
||||||
|
|
||||||
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
createComputed(() => setY(Math.floor(Math.random() * x.length)))
|
||||||
|
@ -20,8 +20,11 @@ export const Row2 = (props: { articles: Shout[] }) => {
|
||||||
{(a, i) => {
|
{(a, i) => {
|
||||||
return (
|
return (
|
||||||
<Show when={!!a}>
|
<Show when={!!a}>
|
||||||
<div class={`col-md-${x[y()][i()]}`}>
|
<div class={`col-md-${props.isEqual ? '6' : x[y()][i()]}`}>
|
||||||
<ArticleCard article={a} settings={{ isWithCover: x[y()][i()] === '8' }} />
|
<ArticleCard
|
||||||
|
article={a}
|
||||||
|
settings={{ isWithCover: props.isEqual || x[y()][i()] === '8' }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { For } from 'solid-js'
|
import { For } from 'solid-js'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useArticlesStore } from '../../stores/zine/articles'
|
import { useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useSeenStore } from '../../stores/zine/seen'
|
import { useSeenStore } from '../../stores/zine/seen'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
type FeedSidebarProps = {
|
type FeedSidebarProps = {
|
||||||
authors: Author[]
|
authors: Author[]
|
||||||
|
@ -14,7 +14,7 @@ type FeedSidebarProps = {
|
||||||
|
|
||||||
export const FeedSidebar = (props: FeedSidebarProps) => {
|
export const FeedSidebar = (props: FeedSidebarProps) => {
|
||||||
const { getSeen: seen } = useSeenStore()
|
const { getSeen: seen } = useSeenStore()
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
||||||
const { articlesByTopic } = useArticlesStore()
|
const { articlesByTopic } = useArticlesStore()
|
||||||
const { topicEntities } = useTopicsStore()
|
const { topicEntities } = useTopicsStore()
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
.swiper-slide {
|
.swiper-slide {
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
padding-top: 100%;
|
|
||||||
|
|
||||||
@include media-breakpoint-up(sm) {
|
.cards-with-cover & {
|
||||||
padding-top: 56.2% !important;
|
height: 0 !important;
|
||||||
}
|
padding-top: 100%;
|
||||||
|
|
||||||
@include media-breakpoint-up(md) {
|
@include media-breakpoint-up(sm) {
|
||||||
padding-top: 35% !important;
|
padding-top: 56.2% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include media-breakpoint-up(md) {
|
||||||
|
padding-top: 35% !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { Icon } from '../Nav/Icon'
|
||||||
interface SliderProps {
|
interface SliderProps {
|
||||||
title?: string
|
title?: string
|
||||||
articles: Shout[]
|
articles: Shout[]
|
||||||
|
slidesPerView?: number
|
||||||
|
isCardsWithCover?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: SliderProps) => {
|
export default (props: SliderProps) => {
|
||||||
|
@ -19,12 +21,14 @@ export default (props: SliderProps) => {
|
||||||
let pagEl: HTMLDivElement | undefined
|
let pagEl: HTMLDivElement | undefined
|
||||||
let nextEl: HTMLDivElement | undefined
|
let nextEl: HTMLDivElement | undefined
|
||||||
let prevEl: HTMLDivElement | undefined
|
let prevEl: HTMLDivElement | undefined
|
||||||
|
|
||||||
|
const isCardsWithCover = typeof props.isCardsWithCover === 'boolean' ? props.isCardsWithCover : true
|
||||||
|
|
||||||
const opts: SwiperOptions = {
|
const opts: SwiperOptions = {
|
||||||
roundLengths: true,
|
roundLengths: true,
|
||||||
loop: true,
|
loop: true,
|
||||||
centeredSlides: true,
|
centeredSlides: true,
|
||||||
slidesPerView: 1,
|
slidesPerView: 1,
|
||||||
spaceBetween: 8,
|
|
||||||
modules: [Navigation, Pagination],
|
modules: [Navigation, Pagination],
|
||||||
speed: 500,
|
speed: 500,
|
||||||
navigation: { nextEl, prevEl },
|
navigation: { nextEl, prevEl },
|
||||||
|
@ -35,7 +39,12 @@ export default (props: SliderProps) => {
|
||||||
},
|
},
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
768: {
|
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)
|
const articles = createMemo(() => props.articles)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="floor floor--important">
|
<div class="floor floor--important">
|
||||||
<div class="wide-container row">
|
<div class="wide-container row">
|
||||||
<h2 class="col-12">{props.title}</h2>
|
<h2 class="col-12">{props.title}</h2>
|
||||||
<Show when={!!articles()}>
|
<Show when={!!articles()}>
|
||||||
<div class="swiper" ref={el}>
|
<div class="swiper" classList={{ 'cards-with-cover': isCardsWithCover }} ref={el}>
|
||||||
<div class="swiper-wrapper">
|
<div class="swiper-wrapper">
|
||||||
<For each={articles()}>
|
<For each={articles()}>
|
||||||
{(a: Shout) => (
|
{(a: Shout) => (
|
||||||
|
@ -63,7 +73,7 @@ export default (props: SliderProps) => {
|
||||||
settings={{
|
settings={{
|
||||||
additionalClass: 'swiper-slide',
|
additionalClass: 'swiper-slide',
|
||||||
isFloorImportant: true,
|
isFloorImportant: true,
|
||||||
isWithCover: true,
|
isWithCover: isCardsWithCover,
|
||||||
nodate: true
|
nodate: true
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -4,14 +4,14 @@ import { t } from '../../../utils/intl'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
import { createMemo, onMount, Show } from 'solid-js'
|
import { createMemo, onMount, Show } from 'solid-js'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import { confirmEmail, useAuthStore } from '../../../stores/auth'
|
import type { ConfirmEmailSearchParams } from './types'
|
||||||
|
import { useAuth } from '../../../context/auth'
|
||||||
type ConfirmEmailSearchParams = {
|
|
||||||
token: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmailConfirm = () => {
|
export const EmailConfirm = () => {
|
||||||
const { session } = useAuthStore()
|
const {
|
||||||
|
session,
|
||||||
|
actions: { confirmEmail }
|
||||||
|
} = useAuth()
|
||||||
|
|
||||||
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
const confirmedEmail = createMemo(() => session()?.user?.email || '')
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ import { useRouter } from '../../../stores/router'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { signSendLink } from '../../../stores/auth'
|
|
||||||
import { locale } from '../../../stores/ui'
|
import { locale } from '../../../stores/ui'
|
||||||
|
import { signSendLink } from '../../../context/auth'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
email: string
|
email: string
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { signIn, signSendLink } from '../../../stores/auth'
|
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createSignal, Show } from 'solid-js'
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
|
@ -10,6 +9,7 @@ import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
import { hideModal, locale } from '../../../stores/ui'
|
import { hideModal, locale } from '../../../stores/ui'
|
||||||
|
import { signSendLink, useAuth } from '../../../context/auth'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
email: string
|
email: string
|
||||||
|
@ -26,6 +26,10 @@ export const LoginForm = () => {
|
||||||
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
|
||||||
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
const [isLinkSent, setIsLinkSent] = createSignal(false)
|
||||||
|
|
||||||
|
const {
|
||||||
|
actions: { signIn }
|
||||||
|
} = useAuth()
|
||||||
|
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
const [password, setPassword] = createSignal('')
|
const [password, setPassword] = createSignal('')
|
||||||
|
|
|
@ -4,13 +4,14 @@ import { t } from '../../../utils/intl'
|
||||||
import styles from './AuthModal.module.scss'
|
import styles from './AuthModal.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { SocialProviders } from './SocialProviders'
|
import { SocialProviders } from './SocialProviders'
|
||||||
import { checkEmail, register, useAuthStore } from '../../../stores/auth'
|
|
||||||
import { isValidEmail } from './validators'
|
import { isValidEmail } from './validators'
|
||||||
import { ApiError } from '../../../utils/apiClient'
|
import { ApiError } from '../../../utils/apiClient'
|
||||||
import { email, setEmail } from './sharedLogic'
|
import { email, setEmail } from './sharedLogic'
|
||||||
import { useRouter } from '../../../stores/router'
|
import { useRouter } from '../../../stores/router'
|
||||||
import type { AuthModalSearchParams } from './types'
|
import type { AuthModalSearchParams } from './types'
|
||||||
import { hideModal } from '../../../stores/ui'
|
import { hideModal } from '../../../stores/ui'
|
||||||
|
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
|
||||||
|
import { register } from '../../../context/auth'
|
||||||
|
|
||||||
type FormFields = {
|
type FormFields = {
|
||||||
name: string
|
name: string
|
||||||
|
@ -23,7 +24,7 @@ type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
|
||||||
export const RegisterForm = () => {
|
export const RegisterForm = () => {
|
||||||
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
const { changeSearchParam } = useRouter<AuthModalSearchParams>()
|
||||||
|
|
||||||
const { emailChecks } = useAuthStore()
|
const { emailChecks } = useEmailChecks()
|
||||||
|
|
||||||
const [submitError, setSubmitError] = createSignal('')
|
const [submitError, setSubmitError] = createSignal('')
|
||||||
const [name, setName] = createSignal('')
|
const [name, setName] = createSignal('')
|
||||||
|
@ -60,11 +61,14 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
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')
|
newValidationErrors.name = t('Please enter a name to sign your comments and publication')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!email()) {
|
if (!clearEmail) {
|
||||||
newValidationErrors.email = t('Please enter email')
|
newValidationErrors.email = t('Please enter email')
|
||||||
} else if (!isValidEmail(email())) {
|
} else if (!isValidEmail(email())) {
|
||||||
newValidationErrors.email = t('Invalid email')
|
newValidationErrors.email = t('Invalid email')
|
||||||
|
@ -76,7 +80,7 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
setValidationErrors(newValidationErrors)
|
setValidationErrors(newValidationErrors)
|
||||||
|
|
||||||
const emailCheckResult = await checkEmail(email())
|
const emailCheckResult = await checkEmail(clearEmail)
|
||||||
|
|
||||||
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
||||||
|
|
||||||
|
@ -88,8 +92,8 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await register({
|
await register({
|
||||||
name: name(),
|
name: clearName,
|
||||||
email: email(),
|
email: clearEmail,
|
||||||
password: password()
|
password: password()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,7 @@ export type AuthModalMode = 'login' | 'register' | 'confirm-email' | 'forgot-pas
|
||||||
export type AuthModalSearchParams = {
|
export type AuthModalSearchParams = {
|
||||||
mode: AuthModalMode
|
mode: AuthModalMode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ConfirmEmailSearchParams = {
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,15 @@
|
||||||
import { For, Show, createSignal, createMemo, createEffect, onMount, onCleanup } from 'solid-js'
|
import { For, Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js'
|
||||||
import Notifications from './Notifications'
|
|
||||||
import { Icon } from './Icon'
|
import { Icon } from './Icon'
|
||||||
import { Modal } from './Modal'
|
import { Modal } from './Modal'
|
||||||
import { AuthModal } from './AuthModal'
|
import { AuthModal } from './AuthModal'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useModalStore, showModal, useWarningsStore } from '../../stores/ui'
|
import { useModalStore } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, router, Routes, useRouter } from '../../stores/router'
|
||||||
import styles from './Header.module.scss'
|
import styles from './Header.module.scss'
|
||||||
import { getPagePath } from '@nanostores/router'
|
import { getPagePath } from '@nanostores/router'
|
||||||
import { getLogger } from '../../utils/logger'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { HeaderAuth } from './HeaderAuth'
|
||||||
import { SharePopup } from '../Article/SharePopup'
|
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 }[] = [
|
const resources: { name: string; route: keyof Routes }[] = [
|
||||||
{ name: t('zine'), route: 'home' },
|
{ name: t('zine'), route: 'home' },
|
||||||
|
@ -34,19 +27,15 @@ export const Header = (props: Props) => {
|
||||||
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
|
||||||
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
const [getIsScrolled, setIsScrolled] = createSignal(false)
|
||||||
const [fixed, setFixed] = createSignal(false)
|
const [fixed, setFixed] = createSignal(false)
|
||||||
const [visibleWarnings, setVisibleWarnings] = createSignal(false)
|
|
||||||
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
|
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
|
||||||
const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false)
|
const [isProfilePopupVisible, setIsProfilePopupVisible] = createSignal(false)
|
||||||
|
|
||||||
// stores
|
|
||||||
const { warnings } = useWarningsStore()
|
|
||||||
const { session } = useAuthStore()
|
|
||||||
const { modal } = useModalStore()
|
const { modal } = useModalStore()
|
||||||
|
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
|
|
||||||
// methods
|
// methods
|
||||||
const toggleWarnings = () => setVisibleWarnings(!visibleWarnings())
|
|
||||||
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
||||||
// effects
|
// 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(() => {
|
onMount(() => {
|
||||||
let scrollTop = window.scrollY
|
let scrollTop = window.scrollY
|
||||||
|
|
||||||
|
@ -146,88 +121,27 @@ export const Header = (props: Props) => {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class={styles.usernav}>
|
<HeaderAuth setIsProfilePopupVisible={setIsProfilePopupVisible} />
|
||||||
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
<Show when={props.title}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={styles.articleControls}>
|
||||||
<a href="/create" onClick={handleClientRouteLinkClick}>
|
<SharePopup
|
||||||
<span class={styles.textLabel}>{t('Create post')}</span>
|
onVisibilityChange={(isVisible) => {
|
||||||
<Icon name="pencil" class={styles.icon} />
|
setIsSharePopupVisible(isVisible)
|
||||||
</a>
|
}}
|
||||||
</div>
|
containerCssClass={styles.control}
|
||||||
|
trigger={<Icon name="share-outline" class={styles.icon} />}
|
||||||
<Show when={authorized()}>
|
/>
|
||||||
<div class={styles.userControlItem}>
|
<a href="#comments" class={styles.control}>
|
||||||
<a href="#" onClick={handleBellIconClick}>
|
<Icon name="comments-outline" class={styles.icon} />
|
||||||
<div>
|
</a>
|
||||||
<Icon name="bell-white" counter={authorized() ? warnings().length : 1} />
|
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
||||||
</div>
|
<Icon name="pencil-outline" class={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
||||||
</Show>
|
<Icon name="bookmark" class={styles.icon} />
|
||||||
|
</a>
|
||||||
<Show when={visibleWarnings()}>
|
|
||||||
<div class={clsx(styles.userControlItem, 'notifications')}>
|
|
||||||
<Notifications />
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={authorized()}
|
|
||||||
fallback={
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
|
||||||
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
|
||||||
<span class={styles.textLabel}>{t('Enter')}</span>
|
|
||||||
<Icon name="user-anonymous" class={styles.icon} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
|
||||||
<a href="/inbox">
|
|
||||||
{/*FIXME: replace with route*/}
|
|
||||||
<div classList={{ entered: page().path === '/inbox' }}>
|
|
||||||
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ProfilePopup
|
|
||||||
onVisibilityChange={(isVisible) => {
|
|
||||||
setIsProfilePopupVisible(isVisible)
|
|
||||||
}}
|
|
||||||
containerCssClass={styles.control}
|
|
||||||
trigger={
|
|
||||||
<div class={styles.userControlItem}>
|
|
||||||
<button class={styles.button}>
|
|
||||||
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
|
||||||
<Userpic user={session().user as Author} class={styles.userpic} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.title}>
|
</Show>
|
||||||
<div class={styles.articleControls}>
|
|
||||||
<SharePopup
|
|
||||||
onVisibilityChange={(isVisible) => {
|
|
||||||
setIsSharePopupVisible(isVisible)
|
|
||||||
}}
|
|
||||||
containerCssClass={styles.control}
|
|
||||||
trigger={<Icon name="share-outline" class={styles.icon} />}
|
|
||||||
/>
|
|
||||||
<a href="#comments" class={styles.control}>
|
|
||||||
<Icon name="comments-outline" class={styles.icon} />
|
|
||||||
</a>
|
|
||||||
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
|
||||||
<Icon name="pencil-outline" class={styles.icon} />
|
|
||||||
</a>
|
|
||||||
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>
|
|
||||||
<Icon name="bookmark" class={styles.icon} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<div class={styles.burgerContainer}>
|
<div class={styles.burgerContainer}>
|
||||||
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
<div class={styles.burger} classList={{ fixed: fixed() }} onClick={toggleFixed}>
|
||||||
<div />
|
<div />
|
||||||
|
|
111
src/components/Nav/HeaderAuth.tsx
Normal file
111
src/components/Nav/HeaderAuth.tsx
Normal file
|
@ -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 (
|
||||||
|
<Show when={isMounted()}>
|
||||||
|
<Show when={!session.loading}>
|
||||||
|
<div class={styles.usernav}>
|
||||||
|
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||||
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
|
<a href="/create" onClick={handleClientRouteLinkClick}>
|
||||||
|
<span class={styles.textLabel}>{t('Create post')}</span>
|
||||||
|
<Icon name="pencil" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={isAuthenticated()}>
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
|
<a href="#" onClick={handleBellIconClick}>
|
||||||
|
<div>
|
||||||
|
<Icon name="bell-white" counter={isAuthenticated() ? warnings().length : 1} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={visibleWarnings()}>
|
||||||
|
<div class={clsx(styles.userControlItem, 'notifications')}>
|
||||||
|
<Notifications />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={isAuthenticated()}
|
||||||
|
fallback={
|
||||||
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose, 'loginbtn')}>
|
||||||
|
<a href="?modal=auth&mode=login" onClick={handleClientRouteLinkClick}>
|
||||||
|
<span class={styles.textLabel}>{t('Enter')}</span>
|
||||||
|
<Icon name="user-anonymous" class={styles.icon} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||||
|
<a href="/inbox">
|
||||||
|
{/*FIXME: replace with route*/}
|
||||||
|
<div classList={{ entered: page().path === '/inbox' }}>
|
||||||
|
<Icon name="inbox-white" counter={session()?.news?.unread || 0} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ProfilePopup
|
||||||
|
onVisibilityChange={(isVisible) => {
|
||||||
|
props.setIsProfilePopupVisible(isVisible)
|
||||||
|
}}
|
||||||
|
containerCssClass={styles.control}
|
||||||
|
trigger={
|
||||||
|
<div class={styles.userControlItem}>
|
||||||
|
<button class={styles.button}>
|
||||||
|
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
||||||
|
<Userpic user={session().user as Author} class={styles.userpic} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,16 +2,19 @@ import { AuthorCard } from '../Author/Card'
|
||||||
import type { Author } from '../../graphql/types.gen'
|
import type { Author } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { hideModal } from '../../stores/ui'
|
import { hideModal } from '../../stores/ui'
|
||||||
import { useAuthStore, signOut } from '../../stores/auth'
|
|
||||||
import { createMemo, For } from 'solid-js'
|
import { createMemo, For } from 'solid-js'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
const quit = () => {
|
export const ProfileModal = () => {
|
||||||
signOut()
|
const {
|
||||||
hideModal()
|
session,
|
||||||
}
|
actions: { signOut }
|
||||||
|
} = useAuth()
|
||||||
|
|
||||||
export default () => {
|
const quit = () => {
|
||||||
const { session } = useAuthStore()
|
signOut()
|
||||||
|
hideModal()
|
||||||
|
}
|
||||||
|
|
||||||
const author = createMemo<Author>(() => {
|
const author = createMemo<Author>(() => {
|
||||||
const a: Author = {
|
const a: Author = {
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { Popup, PopupProps } from './Popup'
|
import { Popup, PopupProps } from './Popup'
|
||||||
import { signOut, useAuthStore } from '../../stores/auth'
|
|
||||||
import styles from './Popup.module.scss'
|
import styles from './Popup.module.scss'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
type ProfilePopupProps = Omit<PopupProps, 'children'>
|
||||||
|
|
||||||
export const ProfilePopup = (props: ProfilePopupProps) => {
|
export const ProfilePopup = (props: ProfilePopupProps) => {
|
||||||
const { session } = useAuthStore()
|
const {
|
||||||
|
session,
|
||||||
|
actions: { signOut }
|
||||||
|
} = useAuth()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props} horizontalAnchor="right">
|
<Popup {...props} horizontalAnchor="right">
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Icon } from './Icon'
|
||||||
import './Topics.scss'
|
import './Topics.scss'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
|
import { handleClientRouteLinkClick } from '../../stores/router'
|
||||||
|
|
||||||
export const NavTopics = (props: { topics: Topic[] }) => {
|
export const NavTopics = (props: { topics: Topic[] }) => {
|
||||||
const tag = (topic: Topic) =>
|
const tag = (topic: Topic) =>
|
||||||
|
@ -17,7 +18,7 @@ export const NavTopics = (props: { topics: Topic[] }) => {
|
||||||
<For each={props.topics}>
|
<For each={props.topics}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<li class="item">
|
<li class="item">
|
||||||
<a href={`/topic/${topic.slug}`}>
|
<a href={`/topic/${topic.slug}`} onClick={handleClientRouteLinkClick}>
|
||||||
<span>#{tag(topic)}</span>
|
<span>#{tag(topic)}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { loadTopic } from '../../stores/zine/topics'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
|
|
||||||
export const TopicPage = (props: PageProps) => {
|
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 slug = createMemo(() => {
|
||||||
const { page: getPage } = useRouter()
|
const { page: getPage } = useRouter()
|
||||||
|
|
|
@ -2,12 +2,11 @@
|
||||||
// import 'solid-devtools'
|
// import 'solid-devtools'
|
||||||
|
|
||||||
import { MODALS, setLocale, showModal } from '../stores/ui'
|
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 { Routes, useRouter } from '../stores/router'
|
||||||
import { Dynamic, isServer } from 'solid-js/web'
|
import { Dynamic, isServer } from 'solid-js/web'
|
||||||
import { getLogger } from '../utils/logger'
|
|
||||||
|
|
||||||
import type { PageProps } from './types'
|
import type { PageProps, RootSearchParams } from './types'
|
||||||
|
|
||||||
import { HomePage } from './Pages/HomePage'
|
import { HomePage } from './Pages/HomePage'
|
||||||
import { AllTopicsPage } from './Pages/AllTopicsPage'
|
import { AllTopicsPage } from './Pages/AllTopicsPage'
|
||||||
|
@ -30,7 +29,7 @@ import { TermsOfUsePage } from './Pages/about/TermsOfUsePage'
|
||||||
import { ThanksPage } from './Pages/about/ThanksPage'
|
import { ThanksPage } from './Pages/about/ThanksPage'
|
||||||
import { CreatePage } from './Pages/CreatePage'
|
import { CreatePage } from './Pages/CreatePage'
|
||||||
import { ConnectPage } from './Pages/ConnectPage'
|
import { ConnectPage } from './Pages/ConnectPage'
|
||||||
import { renewSession } from '../stores/auth'
|
import { AuthProvider } from '../context/auth'
|
||||||
|
|
||||||
// TODO: lazy load
|
// TODO: lazy load
|
||||||
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
// const HomePage = lazy(() => import('./Pages/HomePage'))
|
||||||
|
@ -52,13 +51,6 @@ import { renewSession } from '../stores/auth'
|
||||||
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
// const ThanksPage = lazy(() => import('./Pages/about/ThanksPage'))
|
||||||
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
// const CreatePage = lazy(() => import('./Pages/about/CreatePage'))
|
||||||
|
|
||||||
const log = getLogger('root')
|
|
||||||
|
|
||||||
type RootSearchParams = {
|
|
||||||
modal: string
|
|
||||||
lang: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
const pagesMap: Record<keyof Routes, Component<PageProps>> = {
|
||||||
connect: ConnectPage,
|
connect: ConnectPage,
|
||||||
create: CreatePage,
|
create: CreatePage,
|
||||||
|
@ -92,10 +84,6 @@ export const Root = (props: PageProps) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
renewSession()
|
|
||||||
})
|
|
||||||
|
|
||||||
const pageComponent = createMemo(() => {
|
const pageComponent = createMemo(() => {
|
||||||
const result = pagesMap[page().route]
|
const result = pagesMap[page().route]
|
||||||
|
|
||||||
|
@ -114,5 +102,9 @@ export const Root = (props: PageProps) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Dynamic component={pageComponent()} {...props} />
|
return (
|
||||||
|
<AuthProvider>
|
||||||
|
<Dynamic component={pageComponent()} {...props} />
|
||||||
|
</AuthProvider>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,10 @@ import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { getLogger } from '../../utils/logger'
|
import { getLogger } from '../../utils/logger'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
const log = getLogger('TopicCard')
|
const log = getLogger('TopicCard')
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ interface TopicProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicCard = (props: TopicProps) => {
|
export const TopicCard = (props: TopicProps) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
const subscribed = createMemo(() => {
|
const subscribed = createMemo(() => {
|
||||||
if (!session()?.user?.slug || !session()?.news?.topics) {
|
if (!session()?.user?.slug || !session()?.news?.topics) {
|
||||||
|
|
|
@ -2,17 +2,17 @@ import { createMemo, Show } from 'solid-js'
|
||||||
import type { Topic } from '../../graphql/types.gen'
|
import type { Topic } from '../../graphql/types.gen'
|
||||||
import { FollowingEntity } from '../../graphql/types.gen'
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
import styles from './Full.module.scss'
|
import styles from './Full.module.scss'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import { follow, unfollow } from '../../stores/zine/common'
|
import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullTopic = (props: Props) => {
|
export const FullTopic = (props: Props) => {
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug))
|
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug))
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -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 type { Author } from '../../graphql/types.gen'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
|
import { useAuthorsStore, setAuthorsSort } from '../../stores/zine/authors'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import styles from '../../styles/AllTopics.module.scss'
|
import styles from '../../styles/AllTopics.module.scss'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
type AllAuthorsPageSearchParams = {
|
type AllAuthorsPageSearchParams = {
|
||||||
by: '' | 'name' | 'shouts' | 'rating'
|
by: '' | 'name' | 'shouts' | 'rating'
|
||||||
|
@ -17,10 +17,13 @@ type Props = {
|
||||||
authors: Author[]
|
authors: Author[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
export const AllAuthorsView = (props: Props) => {
|
export const AllAuthorsView = (props: Props) => {
|
||||||
const { sortedAuthors } = useAuthorsStore({ authors: props.authors })
|
const { sortedAuthors } = useAuthorsStore({ authors: props.authors })
|
||||||
|
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
||||||
|
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setAuthorsSort(searchParams().by || 'shouts')
|
setAuthorsSort(searchParams().by || 'shouts')
|
||||||
|
@ -54,7 +57,7 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
return keys
|
return keys
|
||||||
})
|
})
|
||||||
|
|
||||||
// log.debug(getSearchParams())
|
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.allTopicsPage, 'container')}>
|
<div class={clsx(styles.allTopicsPage, 'container')}>
|
||||||
|
@ -95,7 +98,7 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
when={!searchParams().by || searchParams().by === 'name'}
|
when={!searchParams().by || searchParams().by === 'name'}
|
||||||
fallback={() => (
|
fallback={() => (
|
||||||
<div class={styles.stats}>
|
<div class={styles.stats}>
|
||||||
<For each={sortedAuthors()}>
|
<For each={sortedAuthors().slice(0, limit())}>
|
||||||
{(author) => (
|
{(author) => (
|
||||||
<AuthorCard
|
<AuthorCard
|
||||||
author={author}
|
author={author}
|
||||||
|
@ -107,6 +110,13 @@ export const AllAuthorsView = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
<Show when={sortedAuthors().length > limit()}>
|
||||||
|
<div class={styles.loadMoreContainer}>
|
||||||
|
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||||||
|
{t('More')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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 type { Topic } from '../../graphql/types.gen'
|
||||||
import { Icon } from '../Nav/Icon'
|
import { Icon } from '../Nav/Icon'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
import { handleClientRouteLinkClick, useRouter } from '../../stores/router'
|
||||||
import { TopicCard } from '../Topic/Card'
|
import { TopicCard } from '../Topic/Card'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import styles from '../../styles/AllTopics.module.scss'
|
import styles from '../../styles/AllTopics.module.scss'
|
||||||
import cardStyles from '../Topic/Card.module.scss'
|
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
type AllTopicsPageSearchParams = {
|
type AllTopicsPageSearchParams = {
|
||||||
by: 'shouts' | 'authors' | 'title' | ''
|
by: 'shouts' | 'authors' | 'title' | ''
|
||||||
|
@ -18,18 +17,22 @@ type AllTopicsViewProps = {
|
||||||
topics: Topic[]
|
topics: Topic[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PAGE_SIZE = 20
|
||||||
|
|
||||||
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||||
|
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
||||||
|
|
||||||
const { sortedTopics } = useTopicsStore({
|
const { sortedTopics } = useTopicsStore({
|
||||||
topics: props.topics,
|
topics: props.topics,
|
||||||
sortBy: searchParams().by || 'shouts'
|
sortBy: searchParams().by || 'shouts'
|
||||||
})
|
})
|
||||||
|
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setTopicsSort(searchParams().by || 'shouts')
|
setTopicsSort(searchParams().by || 'shouts')
|
||||||
|
setLimit(PAGE_SIZE)
|
||||||
})
|
})
|
||||||
|
|
||||||
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
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 subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || ''))
|
||||||
|
|
||||||
|
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={clsx(styles.allTopicsPage, 'container')}>
|
<div class={clsx(styles.allTopicsPage, 'container')}>
|
||||||
<Show when={sortedTopics().length > 0}>
|
<Show when={sortedTopics().length > 0}>
|
||||||
|
@ -102,9 +107,20 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
<Show
|
<Show
|
||||||
when={searchParams().by === 'title'}
|
when={searchParams().by === 'title'}
|
||||||
fallback={() => (
|
fallback={() => (
|
||||||
<For each={sortedTopics()}>
|
<>
|
||||||
{(topic) => <TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />}
|
<For each={sortedTopics().slice(0, limit())}>
|
||||||
</For>
|
{(topic) => (
|
||||||
|
<TopicCard topic={topic} compact={false} subscribed={subscribed(topic.slug)} />
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Show when={sortedTopics().length > limit()}>
|
||||||
|
<div class={styles.loadMoreContainer}>
|
||||||
|
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||||||
|
{t('More')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<For each={sortedKeys()}>
|
<For each={sortedKeys()}>
|
||||||
|
|
|
@ -8,13 +8,13 @@ import { ArticleCard } from '../Feed/Card'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { FeedSidebar } from '../Feed/Sidebar'
|
import { FeedSidebar } from '../Feed/Sidebar'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
|
||||||
import CommentCard from '../Article/Comment'
|
import CommentCard from '../Article/Comment'
|
||||||
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
|
import { loadRecentArticles, useArticlesStore } from '../../stores/zine/articles'
|
||||||
import { useReactionsStore } from '../../stores/zine/reactions'
|
import { useReactionsStore } from '../../stores/zine/reactions'
|
||||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
|
import { useAuth } from '../../context/auth'
|
||||||
|
|
||||||
// const AUTHORSHIP_REACTIONS = [
|
// const AUTHORSHIP_REACTIONS = [
|
||||||
// ReactionKind.Accept,
|
// ReactionKind.Accept,
|
||||||
|
@ -32,7 +32,7 @@ export const FeedView = () => {
|
||||||
const { sortedAuthors } = useAuthorsStore()
|
const { sortedAuthors } = useAuthorsStore()
|
||||||
const { topTopics } = useTopicsStore()
|
const { topTopics } = useTopicsStore()
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
const { session } = useAuthStore()
|
const { session } = useAuth()
|
||||||
|
|
||||||
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
const topReactions = createMemo(() => sortBy(reactions(), byCreated))
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { FullTopic } from '../Topic/Full'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
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 { useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
import { splitToPages } from '../../utils/splitToPages'
|
import { splitToPages } from '../../utils/splitToPages'
|
||||||
|
@ -26,7 +26,7 @@ interface TopicProps {
|
||||||
topicSlug: string
|
topicSlug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PRERENDERED_ARTICLES_COUNT = 21
|
export const PRERENDERED_ARTICLES_COUNT = 28
|
||||||
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
|
||||||
|
|
||||||
export const TopicView = (props: TopicProps) => {
|
export const TopicView = (props: TopicProps) => {
|
||||||
|
@ -44,7 +44,8 @@ export const TopicView = (props: TopicProps) => {
|
||||||
const loadMore = async () => {
|
const loadMore = async () => {
|
||||||
saveScrollPosition()
|
saveScrollPosition()
|
||||||
|
|
||||||
const { hasMore } = await loadPublishedArticles({
|
const { hasMore } = await loadTopicArticles({
|
||||||
|
topicSlug: topic().slug,
|
||||||
limit: LOAD_MORE_PAGE_SIZE,
|
limit: LOAD_MORE_PAGE_SIZE,
|
||||||
offset: sortedArticles().length
|
offset: sortedArticles().length
|
||||||
})
|
})
|
||||||
|
@ -112,7 +113,7 @@ export const TopicView = (props: TopicProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Row1 article={sortedArticles()[0]} />
|
<Row1 article={sortedArticles()[0]} />
|
||||||
<Row2 articles={sortedArticles().slice(1, 3)} />
|
<Row2 articles={sortedArticles().slice(1, 3)} isEqual={true} />
|
||||||
|
|
||||||
<Beside
|
<Beside
|
||||||
title={t('Topic is supported by')}
|
title={t('Topic is supported by')}
|
||||||
|
@ -130,13 +131,20 @@ export const TopicView = (props: TopicProps) => {
|
||||||
wrapper={'top-article'}
|
wrapper={'top-article'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Show when={sortedArticles().length > 5}>
|
|
||||||
<Row3 articles={sortedArticles().slice(13, 16)} />
|
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
||||||
<Row2 articles={sortedArticles().slice(16, 18)} />
|
<Row1 article={sortedArticles()[15]} />
|
||||||
<Row3 articles={sortedArticles().slice(18, 21)} />
|
|
||||||
<Row3 articles={sortedArticles().slice(21, 24)} />
|
<Slider
|
||||||
<Row3 articles={sortedArticles().slice(24, 27)} />
|
title={title()}
|
||||||
</Show>
|
articles={sortedArticles().slice(16, 22)}
|
||||||
|
slidesPerView={3}
|
||||||
|
isCardsWithCover={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Row3 articles={sortedArticles().slice(23, 26)} />
|
||||||
|
<Row2 articles={sortedArticles().slice(26, 28)} />
|
||||||
|
|
||||||
|
|
||||||
<For each={pages()}>
|
<For each={pages()}>
|
||||||
{(page) => (
|
{(page) => (
|
||||||
|
|
|
@ -17,3 +17,8 @@ export type PageProps = {
|
||||||
searchResults?: Shout[]
|
searchResults?: Shout[]
|
||||||
chats?: Chat[]
|
chats?: Chat[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RootSearchParams = {
|
||||||
|
modal: string
|
||||||
|
lang: string
|
||||||
|
}
|
||||||
|
|
98
src/context/auth.tsx
Normal file
98
src/context/auth.tsx
Normal file
|
@ -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<AuthResult>
|
||||||
|
isAuthenticated: Accessor<boolean>
|
||||||
|
actions: {
|
||||||
|
refreshSession: () => AuthResult | Promise<AuthResult>
|
||||||
|
signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
|
||||||
|
signOut: () => Promise<void>
|
||||||
|
confirmEmail: (token: string) => Promise<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType>()
|
||||||
|
|
||||||
|
const refreshSession = async (): Promise<AuthResult> => {
|
||||||
|
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<AuthResult>(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 <AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>
|
||||||
|
}
|
|
@ -27,7 +27,6 @@ const options: ClientOptions = {
|
||||||
// меняем через setToken, например при получении значения с сервера
|
// меняем через setToken, например при получении значения с сервера
|
||||||
// скорее всего придумаем что-нибудь получше со временем
|
// скорее всего придумаем что-нибудь получше со временем
|
||||||
const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
|
const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
|
||||||
|
|
||||||
const headers = { Auth: token }
|
const headers = { Auth: token }
|
||||||
return { headers }
|
return { headers }
|
||||||
},
|
},
|
||||||
|
|
22
src/graphql/query/author-by-slug.ts
Normal file
22
src/graphql/query/author-by-slug.ts
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -8,14 +8,11 @@ export default gql`
|
||||||
name
|
name
|
||||||
bio
|
bio
|
||||||
userpic
|
userpic
|
||||||
communities
|
|
||||||
links
|
links
|
||||||
createdAt
|
|
||||||
lastSeen
|
lastSeen
|
||||||
ratings {
|
stat {
|
||||||
_id: rater
|
followers
|
||||||
rater
|
followings
|
||||||
value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
22
src/graphql/query/topic-by-slug.ts
Normal file
22
src/graphql/query/topic-by-slug.ts
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -25,18 +25,30 @@ export type Author = {
|
||||||
bio?: Maybe<Scalars['String']>
|
bio?: Maybe<Scalars['String']>
|
||||||
caption?: Maybe<Scalars['String']>
|
caption?: Maybe<Scalars['String']>
|
||||||
id: Scalars['Int']
|
id: Scalars['Int']
|
||||||
|
lastSeen?: Maybe<Scalars['DateTime']>
|
||||||
links?: Maybe<Array<Maybe<Scalars['String']>>>
|
links?: Maybe<Array<Maybe<Scalars['String']>>>
|
||||||
name: Scalars['String']
|
name: Scalars['String']
|
||||||
|
roles?: Maybe<Array<Maybe<Role>>>
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
|
stat?: Maybe<AuthorStat>
|
||||||
userpic?: Maybe<Scalars['String']>
|
userpic?: Maybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthorStat = {
|
||||||
|
commented?: Maybe<Scalars['Int']>
|
||||||
|
followers?: Maybe<Scalars['Int']>
|
||||||
|
followings?: Maybe<Scalars['Int']>
|
||||||
|
rating?: Maybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
export type Chat = {
|
export type Chat = {
|
||||||
|
admins?: Maybe<Array<Maybe<User>>>
|
||||||
createdAt: Scalars['Int']
|
createdAt: Scalars['Int']
|
||||||
createdBy: User
|
createdBy: User
|
||||||
description?: Maybe<Scalars['String']>
|
description?: Maybe<Scalars['String']>
|
||||||
id: Scalars['String']
|
id: Scalars['String']
|
||||||
messages: Array<Maybe<Message>>
|
messages: Array<Maybe<Message>>
|
||||||
|
private?: Maybe<Scalars['Boolean']>
|
||||||
title?: Maybe<Scalars['String']>
|
title?: Maybe<Scalars['String']>
|
||||||
unread?: Maybe<Scalars['Int']>
|
unread?: Maybe<Scalars['Int']>
|
||||||
updatedAt: Scalars['Int']
|
updatedAt: Scalars['Int']
|
||||||
|
@ -53,8 +65,8 @@ export type ChatMember = {
|
||||||
invitedAt?: Maybe<Scalars['DateTime']>
|
invitedAt?: Maybe<Scalars['DateTime']>
|
||||||
invitedBy?: Maybe<Scalars['String']>
|
invitedBy?: Maybe<Scalars['String']>
|
||||||
name: Scalars['String']
|
name: Scalars['String']
|
||||||
pic?: Maybe<Scalars['String']>
|
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
|
userpic?: Maybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Collab = {
|
export type Collab = {
|
||||||
|
@ -130,6 +142,7 @@ export type Mutation = {
|
||||||
createReaction: Result
|
createReaction: Result
|
||||||
createShout: Result
|
createShout: Result
|
||||||
createTopic: Result
|
createTopic: Result
|
||||||
|
deleteChat: Result
|
||||||
deleteCollection: Result
|
deleteCollection: Result
|
||||||
deleteCommunity: Result
|
deleteCommunity: Result
|
||||||
deleteMessage: Result
|
deleteMessage: Result
|
||||||
|
@ -193,6 +206,10 @@ export type MutationCreateTopicArgs = {
|
||||||
input: TopicInput
|
input: TopicInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationDeleteChatArgs = {
|
||||||
|
chatId: Scalars['String']
|
||||||
|
}
|
||||||
|
|
||||||
export type MutationDeleteCollectionArgs = {
|
export type MutationDeleteCollectionArgs = {
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
}
|
}
|
||||||
|
@ -330,7 +347,7 @@ export type ProfileInput = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
authorsAll: Array<Maybe<User>>
|
authorsAll: Array<Maybe<Author>>
|
||||||
collectionsAll: Array<Maybe<Collection>>
|
collectionsAll: Array<Maybe<Collection>>
|
||||||
getAuthor: User
|
getAuthor: User
|
||||||
getCollabs: Array<Maybe<Collab>>
|
getCollabs: Array<Maybe<Collab>>
|
||||||
|
@ -340,39 +357,46 @@ export type Query = {
|
||||||
getTopic: Topic
|
getTopic: Topic
|
||||||
getUserCollections: Array<Maybe<Collection>>
|
getUserCollections: Array<Maybe<Collection>>
|
||||||
getUserRoles: Array<Maybe<Role>>
|
getUserRoles: Array<Maybe<Role>>
|
||||||
getUsersBySlugs: Array<Maybe<User>>
|
getUsersBySlugs: Array<Maybe<Author>>
|
||||||
isEmailUsed: Scalars['Boolean']
|
isEmailUsed: Scalars['Boolean']
|
||||||
loadChat: Result
|
loadChats: Result
|
||||||
|
loadMessages: Result
|
||||||
markdownBody: Scalars['String']
|
markdownBody: Scalars['String']
|
||||||
myChats: Result
|
|
||||||
reactionsByAuthor: Array<Maybe<Reaction>>
|
reactionsByAuthor: Array<Maybe<Reaction>>
|
||||||
reactionsForShouts: Array<Maybe<Reaction>>
|
reactionsForShouts: Array<Maybe<Reaction>>
|
||||||
recentAll: Array<Maybe<Shout>>
|
recentAll: Array<Maybe<Shout>>
|
||||||
recentCandidates: Array<Maybe<Shout>>
|
recentCandidates: Array<Maybe<Shout>>
|
||||||
recentCommented: Array<Maybe<Shout>>
|
recentCommented: Array<Maybe<Shout>>
|
||||||
|
recentLayoutShouts: Array<Maybe<Shout>>
|
||||||
recentPublished: Array<Maybe<Shout>>
|
recentPublished: Array<Maybe<Shout>>
|
||||||
recentReacted: Array<Maybe<Shout>>
|
recentReacted: Array<Maybe<Shout>>
|
||||||
|
searchChats: Result
|
||||||
|
searchMessages: Result
|
||||||
searchQuery?: Maybe<Array<Maybe<Shout>>>
|
searchQuery?: Maybe<Array<Maybe<Shout>>>
|
||||||
|
searchUsers: Result
|
||||||
shoutsByAuthors: Array<Maybe<Shout>>
|
shoutsByAuthors: Array<Maybe<Shout>>
|
||||||
shoutsByCollection: Array<Maybe<Shout>>
|
shoutsByCollection: Array<Maybe<Shout>>
|
||||||
shoutsByCommunities: Array<Maybe<Shout>>
|
shoutsByCommunities: Array<Maybe<Shout>>
|
||||||
|
shoutsByLayout: Array<Maybe<Shout>>
|
||||||
shoutsByTopics: Array<Maybe<Shout>>
|
shoutsByTopics: Array<Maybe<Shout>>
|
||||||
shoutsForFeed: Array<Maybe<Shout>>
|
shoutsForFeed: Array<Maybe<Shout>>
|
||||||
signIn: AuthResult
|
signIn: AuthResult
|
||||||
signOut: AuthResult
|
signOut: AuthResult
|
||||||
topAuthors: Array<Maybe<Author>>
|
topAuthors: Array<Maybe<Author>>
|
||||||
topCommented: Array<Maybe<Shout>>
|
topCommented: Array<Maybe<Shout>>
|
||||||
|
topLayoutShouts: Array<Maybe<Shout>>
|
||||||
topMonth: Array<Maybe<Shout>>
|
topMonth: Array<Maybe<Shout>>
|
||||||
|
topMonthLayoutShouts: Array<Maybe<Shout>>
|
||||||
topOverall: Array<Maybe<Shout>>
|
topOverall: Array<Maybe<Shout>>
|
||||||
topPublished: Array<Maybe<Shout>>
|
topPublished: Array<Maybe<Shout>>
|
||||||
topicsAll: Array<Maybe<Topic>>
|
topicsAll: Array<Maybe<Topic>>
|
||||||
topicsByAuthor: Array<Maybe<Topic>>
|
topicsByAuthor: Array<Maybe<Topic>>
|
||||||
topicsByCommunity: Array<Maybe<Topic>>
|
topicsByCommunity: Array<Maybe<Topic>>
|
||||||
topicsRandom: Array<Maybe<Topic>>
|
topicsRandom: Array<Maybe<Topic>>
|
||||||
userFollowedAuthors: Array<Maybe<User>>
|
userFollowedAuthors: Array<Maybe<Author>>
|
||||||
userFollowedCommunities: Array<Maybe<Community>>
|
userFollowedCommunities: Array<Maybe<Community>>
|
||||||
userFollowedTopics: Array<Maybe<Topic>>
|
userFollowedTopics: Array<Maybe<Topic>>
|
||||||
userFollowers: Array<Maybe<User>>
|
userFollowers: Array<Maybe<Author>>
|
||||||
userReactedShouts: Array<Maybe<Shout>>
|
userReactedShouts: Array<Maybe<Shout>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -408,7 +432,12 @@ export type QueryIsEmailUsedArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QueryLoadChatArgs = {
|
export type QueryLoadChatsArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryLoadMessagesArgs = {
|
||||||
amount?: InputMaybe<Scalars['Int']>
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
chatId: Scalars['String']
|
chatId: Scalars['String']
|
||||||
offset?: InputMaybe<Scalars['Int']>
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
@ -445,6 +474,12 @@ export type QueryRecentCommentedArgs = {
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryRecentLayoutShoutsArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
layout: Scalars['String']
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryRecentPublishedArgs = {
|
export type QueryRecentPublishedArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
|
@ -455,12 +490,30 @@ export type QueryRecentReactedArgs = {
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QuerySearchChatsArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
q: Scalars['String']
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QuerySearchMessagesArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
q: Scalars['String']
|
||||||
|
}
|
||||||
|
|
||||||
export type QuerySearchQueryArgs = {
|
export type QuerySearchQueryArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
q?: InputMaybe<Scalars['String']>
|
q?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QuerySearchUsersArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
q: Scalars['String']
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryShoutsByAuthorsArgs = {
|
export type QueryShoutsByAuthorsArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
|
@ -479,6 +532,12 @@ export type QueryShoutsByCommunitiesArgs = {
|
||||||
slugs: Array<InputMaybe<Scalars['String']>>
|
slugs: Array<InputMaybe<Scalars['String']>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryShoutsByLayoutArgs = {
|
||||||
|
amount: Scalars['Int']
|
||||||
|
layout?: InputMaybe<Scalars['String']>
|
||||||
|
offset: Scalars['Int']
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryShoutsByTopicsArgs = {
|
export type QueryShoutsByTopicsArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
|
@ -506,11 +565,23 @@ export type QueryTopCommentedArgs = {
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryTopLayoutShoutsArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
layout: Scalars['String']
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryTopMonthArgs = {
|
export type QueryTopMonthArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryTopMonthLayoutShoutsArgs = {
|
||||||
|
amount?: InputMaybe<Scalars['Int']>
|
||||||
|
layout: Scalars['String']
|
||||||
|
offset?: InputMaybe<Scalars['Int']>
|
||||||
|
}
|
||||||
|
|
||||||
export type QueryTopOverallArgs = {
|
export type QueryTopOverallArgs = {
|
||||||
limit: Scalars['Int']
|
limit: Scalars['Int']
|
||||||
offset: Scalars['Int']
|
offset: Scalars['Int']
|
||||||
|
@ -619,8 +690,8 @@ export type Resource = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Result = {
|
export type Result = {
|
||||||
author?: Maybe<User>
|
author?: Maybe<Author>
|
||||||
authors?: Maybe<Array<Maybe<User>>>
|
authors?: Maybe<Array<Maybe<Author>>>
|
||||||
chat?: Maybe<Chat>
|
chat?: Maybe<Chat>
|
||||||
chats?: Maybe<Array<Maybe<Chat>>>
|
chats?: Maybe<Array<Maybe<Chat>>>
|
||||||
communities?: Maybe<Array<Maybe<Community>>>
|
communities?: Maybe<Array<Maybe<Community>>>
|
||||||
|
@ -633,8 +704,10 @@ export type Result = {
|
||||||
reactions?: Maybe<Array<Maybe<Reaction>>>
|
reactions?: Maybe<Array<Maybe<Reaction>>>
|
||||||
shout?: Maybe<Shout>
|
shout?: Maybe<Shout>
|
||||||
shouts?: Maybe<Array<Maybe<Shout>>>
|
shouts?: Maybe<Array<Maybe<Shout>>>
|
||||||
|
slugs?: Maybe<Array<Maybe<Scalars['String']>>>
|
||||||
topic?: Maybe<Topic>
|
topic?: Maybe<Topic>
|
||||||
topics?: Maybe<Array<Maybe<Topic>>>
|
topics?: Maybe<Array<Maybe<Topic>>>
|
||||||
|
uids?: Maybe<Array<Maybe<Scalars['String']>>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Role = {
|
export type Role = {
|
||||||
|
@ -653,11 +726,11 @@ export type Shout = {
|
||||||
createdAt: Scalars['DateTime']
|
createdAt: Scalars['DateTime']
|
||||||
deletedAt?: Maybe<Scalars['DateTime']>
|
deletedAt?: Maybe<Scalars['DateTime']>
|
||||||
deletedBy?: Maybe<User>
|
deletedBy?: Maybe<User>
|
||||||
draft?: Maybe<Scalars['Boolean']>
|
|
||||||
id: Scalars['Int']
|
id: Scalars['Int']
|
||||||
lang?: Maybe<Scalars['String']>
|
lang?: Maybe<Scalars['String']>
|
||||||
layout?: Maybe<Scalars['String']>
|
layout?: Maybe<Scalars['String']>
|
||||||
mainTopic?: Maybe<Scalars['String']>
|
mainTopic?: Maybe<Scalars['String']>
|
||||||
|
media?: Maybe<Scalars['String']>
|
||||||
publishedAt?: Maybe<Scalars['DateTime']>
|
publishedAt?: Maybe<Scalars['DateTime']>
|
||||||
publishedBy?: Maybe<User>
|
publishedBy?: Maybe<User>
|
||||||
slug: Scalars['String']
|
slug: Scalars['String']
|
||||||
|
@ -667,8 +740,8 @@ export type Shout = {
|
||||||
topics?: Maybe<Array<Maybe<Topic>>>
|
topics?: Maybe<Array<Maybe<Topic>>>
|
||||||
updatedAt?: Maybe<Scalars['DateTime']>
|
updatedAt?: Maybe<Scalars['DateTime']>
|
||||||
updatedBy?: Maybe<User>
|
updatedBy?: Maybe<User>
|
||||||
versionOf?: Maybe<Shout>
|
versionOf?: Maybe<Scalars['String']>
|
||||||
visibleFor?: Maybe<Array<Maybe<User>>>
|
visibility?: Maybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ShoutInput = {
|
export type ShoutInput = {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author'
|
||||||
|
|
||||||
const slug = Astro.params.slug.toString()
|
const slug = Astro.params.slug.toString()
|
||||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
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
|
const { pathname, search } = Astro.url
|
||||||
initRouter(pathname, search)
|
initRouter(pathname, search)
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic'
|
||||||
|
|
||||||
const slug = Astro.params.slug?.toString() || ''
|
const slug = Astro.params.slug?.toString() || ''
|
||||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
|
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'
|
import { initRouter } from '../../stores/router'
|
||||||
|
|
||||||
|
|
|
@ -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<AuthResult | null>(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<boolean> => {
|
|
||||||
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 }
|
|
||||||
}
|
|
23
src/stores/emailChecks.ts
Normal file
23
src/stores/emailChecks.ts
Normal file
|
@ -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<boolean> => {
|
||||||
|
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 }
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
//import { persistentAtom } from '@nanostores/persistent'
|
//import { persistentAtom } from '@nanostores/persistent'
|
||||||
import { createSignal } from 'solid-js'
|
import { createSignal } from 'solid-js'
|
||||||
import { useRouter } from './router'
|
import { useRouter } from './router'
|
||||||
|
import type { AuthModalSearchParams, ConfirmEmailSearchParams } from '../components/Nav/AuthModal/types'
|
||||||
|
import type { RootSearchParams } from '../components/types'
|
||||||
|
|
||||||
//export const locale = persistentAtom<string>('locale', 'ru')
|
//export const locale = persistentAtom<string>('locale', 'ru')
|
||||||
export const [locale, setLocale] = createSignal('ru')
|
export const [locale, setLocale] = createSignal('ru')
|
||||||
|
@ -26,10 +28,22 @@ const [modal, setModal] = createSignal<ModalType | null>(null)
|
||||||
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
const [warnings, setWarnings] = createSignal<Warning[]>([])
|
||||||
|
|
||||||
export const showModal = (modalType: ModalType) => setModal(modalType)
|
export const showModal = (modalType: ModalType) => setModal(modalType)
|
||||||
|
|
||||||
|
// TODO: find a better solution
|
||||||
export const hideModal = () => {
|
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('modal', null, true)
|
||||||
changeSearchParam('mode', null, true)
|
|
||||||
setModal(null)
|
setModal(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,9 +52,7 @@ const addAuthors = (authors: Author[]) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
|
export const loadAuthor = async ({ slug }: { slug: string }): Promise<void> => {
|
||||||
// TODO:
|
const author = await apiClient.getAuthor({ slug })
|
||||||
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 1 })
|
|
||||||
const author = articles[0].authors.find((a) => a.slug === slug)
|
|
||||||
addAuthors([author])
|
addAuthors([author])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -100,9 +100,7 @@ export const loadRandomTopics = async (): Promise<void> => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadTopic = async ({ slug }: { slug: string }): Promise<void> => {
|
export const loadTopic = async ({ slug }: { slug: string }): Promise<void> => {
|
||||||
// TODO:
|
const topic = await apiClient.getTopic({ slug })
|
||||||
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 1 })
|
|
||||||
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
|
|
||||||
addTopics([topic])
|
addTopics([topic])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,3 +35,12 @@
|
||||||
.stats {
|
.stats {
|
||||||
margin-top: 2.4rem;
|
margin-top: 2.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loadMoreContainer {
|
||||||
|
margin-top: 48px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.loadMoreButton {
|
||||||
|
padding: 0.6em 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -663,7 +663,7 @@ astro-island {
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
@include media-breakpoint-down(sm) {
|
@include media-breakpoint-down(sm) {
|
||||||
//padding: 0 $container-padding-x * 0.5;
|
// padding: 0 $container-padding-x * 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 { publicGraphQLClient } from '../graphql/publicGraphQLClient'
|
||||||
import { privateGraphQLClient } from '../graphql/privateGraphQLClient'
|
import { privateGraphQLClient } from '../graphql/privateGraphQLClient'
|
||||||
import articleBySlug from '../graphql/query/article-by-slug'
|
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 reactionCreate from '../graphql/mutation/reaction-create'
|
||||||
import reactionDestroy from '../graphql/mutation/reaction-destroy'
|
import reactionDestroy from '../graphql/mutation/reaction-destroy'
|
||||||
import reactionUpdate from '../graphql/mutation/reaction-update'
|
import reactionUpdate from '../graphql/mutation/reaction-update'
|
||||||
import authorsBySlugs from '../graphql/query/authors-by-slugs'
|
|
||||||
import incrementView from '../graphql/mutation/increment-view'
|
import incrementView from '../graphql/mutation/increment-view'
|
||||||
import createArticle from '../graphql/mutation/article-create'
|
import createArticle from '../graphql/mutation/article-create'
|
||||||
import myChats from '../graphql/query/my-chats'
|
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
|
const FEED_SIZE = 50
|
||||||
|
|
||||||
|
@ -44,7 +53,7 @@ export class ApiError extends Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = {
|
export const apiClient = {
|
||||||
authLogin: async ({ email, password }): Promise<AuthResult> => {
|
authLogin: async ({ email, password }: { email: string; password: string }): Promise<AuthResult> => {
|
||||||
const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
|
const response = await publicGraphQLClient.query(authLoginQuery, { email, password }).toPromise()
|
||||||
// console.debug('[api-client] authLogin', { response })
|
// console.debug('[api-client] authLogin', { response })
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
|
@ -98,6 +107,11 @@ export const apiClient = {
|
||||||
authSendLink: async ({ email, lang }) => {
|
authSendLink: async ({ email, lang }) => {
|
||||||
// send link with code on email
|
// send link with code on email
|
||||||
const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise()
|
const response = await publicGraphQLClient.mutation(authSendLinkMutation, { email, lang }).toPromise()
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new ApiError('unknown', response.error.message)
|
||||||
|
}
|
||||||
|
|
||||||
return response.data.sendLink
|
return response.data.sendLink
|
||||||
},
|
},
|
||||||
confirmEmail: async ({ token }: { token: string }) => {
|
confirmEmail: async ({ token }: { token: string }) => {
|
||||||
|
@ -241,7 +255,6 @@ export const apiClient = {
|
||||||
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
const response = await privateGraphQLClient.mutation(mySession, {}).toPromise()
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
// TODO
|
|
||||||
throw new ApiError('unknown', response.error.message)
|
throw new ApiError('unknown', response.error.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,9 +287,13 @@ export const apiClient = {
|
||||||
}
|
}
|
||||||
return response.data.authorsAll
|
return response.data.authorsAll
|
||||||
},
|
},
|
||||||
getAuthor: async ({ slug }: { slug: string }) => {
|
getAuthor: async ({ slug }: { slug: string }): Promise<Author> => {
|
||||||
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs: [slug] }).toPromise()
|
const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise()
|
||||||
return response.data.getUsersBySlugs
|
return response.data.getAuthor
|
||||||
|
},
|
||||||
|
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
|
||||||
|
const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise()
|
||||||
|
return response.data.getTopic
|
||||||
},
|
},
|
||||||
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
|
getArticle: async ({ slug }: { slug: string }): Promise<Shout> => {
|
||||||
const response = await publicGraphQLClient.query(articleBySlug, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(articleBySlug, { slug }).toPromise()
|
||||||
|
@ -304,10 +321,6 @@ export const apiClient = {
|
||||||
|
|
||||||
return response.data.reactionsForShouts
|
return response.data.reactionsForShouts
|
||||||
},
|
},
|
||||||
getAuthorsBySlugs: async ({ slugs }) => {
|
|
||||||
const response = await publicGraphQLClient.query(authorsBySlugs, { slugs }).toPromise()
|
|
||||||
return response.data.getUsersBySlugs
|
|
||||||
},
|
|
||||||
createArticle: async ({ article }: { article: ShoutInput }) => {
|
createArticle: async ({ article }: { article: ShoutInput }) => {
|
||||||
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
const response = await privateGraphQLClient.mutation(createArticle, { shout: article }).toPromise()
|
||||||
console.debug('createArticle response:', response)
|
console.debug('createArticle response:', response)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const isDev = import.meta.env.MODE === 'development'
|
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'
|
// export const apiBaseUrl = 'http://localhost:8080'
|
||||||
|
|
Loading…
Reference in New Issue
Block a user