home page tops fix, all authors, all topics fixes and more
This commit is contained in:
parent
c086a242b7
commit
e61181392c
|
@ -2,14 +2,14 @@ import { capitalize, formatDate } from '../../utils'
|
||||||
import './Full.scss'
|
import './Full.scss'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { createMemo, createSignal, For, Match, onMount, Show, Switch } from 'solid-js'
|
import { createMemo, For, Match, onMount, Show, Switch } from 'solid-js'
|
||||||
import type { Author, Shout } from '../../graphql/types.gen'
|
import type { Author, Shout } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import MD from './MD'
|
import MD from './MD'
|
||||||
import { SharePopup } from './SharePopup'
|
import { SharePopup } from './SharePopup'
|
||||||
import stylesHeader from '../Nav/Header.module.scss'
|
import stylesHeader from '../Nav/Header.module.scss'
|
||||||
import styles from '../../styles/Article.module.scss'
|
import styles from '../../styles/Article.module.scss'
|
||||||
import RatingControl from './RatingControl'
|
import { RatingControl } from './RatingControl'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { CommentsTree } from './CommentsTree'
|
import { CommentsTree } from './CommentsTree'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
|
|
|
@ -20,5 +20,3 @@ export const RatingControl = (props: RatingControlProps) => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RatingControl
|
|
||||||
|
|
|
@ -277,3 +277,7 @@
|
||||||
margin-top: -0.6em;
|
margin-top: -0.6em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.isSubscribing {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { Author } from '../../graphql/types.gen'
|
||||||
import Userpic from './Userpic'
|
import Userpic from './Userpic'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import styles from './Card.module.scss'
|
import styles from './Card.module.scss'
|
||||||
import { createMemo, For, Show } from 'solid-js'
|
import { createMemo, createSignal, 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 { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
|
@ -10,6 +10,8 @@ import { follow, unfollow } from '../../stores/zine/common'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { StatMetrics } from '../_shared/StatMetrics'
|
import { StatMetrics } from '../_shared/StatMetrics'
|
||||||
|
import { FollowingEntity } from '../../graphql/types.gen'
|
||||||
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
|
||||||
interface AuthorCardProps {
|
interface AuthorCardProps {
|
||||||
caption?: string
|
caption?: string
|
||||||
|
@ -28,17 +30,32 @@ interface AuthorCardProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthorCard = (props: AuthorCardProps) => {
|
export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
const { session } = useSession()
|
const {
|
||||||
|
session,
|
||||||
|
actions: { loadSession }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
|
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const subscribe = async (really = true) => {
|
||||||
|
setIsSubscribing(true)
|
||||||
|
|
||||||
|
await (really
|
||||||
|
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
||||||
|
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
||||||
|
|
||||||
|
await loadSession()
|
||||||
|
setIsSubscribing(false)
|
||||||
|
}
|
||||||
|
|
||||||
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
|
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug)
|
||||||
|
|
||||||
const name = () => {
|
const name = () => {
|
||||||
return props.author.name === 'Дискурс' && locale() !== 'ru'
|
return props.author.name === 'Дискурс' && locale() !== 'ru' ? 'Discours' : translit(props.author.name)
|
||||||
? 'Discours'
|
|
||||||
: translit(props.author.name || '', locale() || 'ru')
|
|
||||||
}
|
}
|
||||||
// TODO: reimplement AuthorCard
|
// TODO: reimplement AuthorCard
|
||||||
return (
|
return (
|
||||||
|
@ -75,28 +92,31 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
class={styles.authorAbout}
|
class={styles.authorAbout}
|
||||||
classList={{ 'text-truncate': props.truncateBio }}
|
classList={{ 'text-truncate': props.truncateBio }}
|
||||||
innerHTML={props.author.bio}
|
innerHTML={props.author.bio}
|
||||||
></div>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={props.author.stat}>
|
<Show when={props.author.stat}>
|
||||||
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={props.author.stat} />
|
<StatMetrics fields={['shouts', 'followers', 'comments']} stat={props.author.stat} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
<ShowOnlyOnClient>
|
||||||
|
<Show when={session.state !== 'pending'}>
|
||||||
<Show when={canFollow()}>
|
<Show when={canFollow()}>
|
||||||
<div class={styles.authorSubscribe}>
|
<div class={styles.authorSubscribe}>
|
||||||
<Show
|
<Show
|
||||||
when={subscribed()}
|
when={subscribed()}
|
||||||
fallback={
|
fallback={
|
||||||
<button
|
<button
|
||||||
onClick={() => follow}
|
onClick={() => subscribe(true)}
|
||||||
class={clsx('button', styles.button)}
|
class={clsx('button', styles.button)}
|
||||||
classList={{
|
classList={{
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList,
|
[styles.buttonSubscribe]: !props.isAuthorsList,
|
||||||
'button--subscribe': !props.isAuthorsList,
|
'button--subscribe': !props.isAuthorsList,
|
||||||
'button--subscribe-topic': props.isAuthorsList,
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
[styles.buttonWrite]: props.isAuthorsList
|
[styles.buttonWrite]: props.isAuthorsList,
|
||||||
|
[styles.isSubscribing]: isSubscribing()
|
||||||
}}
|
}}
|
||||||
|
disabled={isSubscribing()}
|
||||||
>
|
>
|
||||||
<Show when={!props.isAuthorsList}>
|
<Show when={!props.isAuthorsList}>
|
||||||
<Icon name="circle-plus" iconClassName={styles.s24} class={styles.icon} />
|
<Icon name="circle-plus" iconClassName={styles.s24} class={styles.icon} />
|
||||||
|
@ -106,13 +126,16 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => unfollow}
|
onClick={() => subscribe(false)}
|
||||||
|
class={clsx('button', styles.button)}
|
||||||
classList={{
|
classList={{
|
||||||
[styles.buttonSubscribe]: !props.isAuthorsList,
|
[styles.buttonSubscribe]: !props.isAuthorsList,
|
||||||
'button--subscribe': !props.isAuthorsList,
|
'button--subscribe': !props.isAuthorsList,
|
||||||
'button--subscribe-topic': props.isAuthorsList,
|
'button--subscribe-topic': props.isAuthorsList,
|
||||||
[styles.buttonWrite]: props.isAuthorsList
|
[styles.buttonWrite]: props.isAuthorsList,
|
||||||
|
[styles.isSubscribing]: isSubscribing()
|
||||||
}}
|
}}
|
||||||
|
disabled={isSubscribing()}
|
||||||
>
|
>
|
||||||
<Show when={!props.isAuthorsList}>
|
<Show when={!props.isAuthorsList}>
|
||||||
<Icon name="author-unsubscribe" class={styles.icon} />
|
<Icon name="author-unsubscribe" class={styles.icon} />
|
||||||
|
@ -141,6 +164,8 @@ export const AuthorCard = (props: AuthorCardProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</ShowOnlyOnClient>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -59,7 +59,7 @@ export const Editor = () => {
|
||||||
const handleSaveButtonClick = () => {
|
const handleSaveButtonClick = () => {
|
||||||
const article: ShoutInput = {
|
const article: ShoutInput = {
|
||||||
body: getHtml(editorViewRef.current.state),
|
body: getHtml(editorViewRef.current.state),
|
||||||
community: 'discours', // ?
|
community: 1, // 'discours' ?
|
||||||
slug: 'new-' + Math.floor(Math.random() * 1000000)
|
slug: 'new-' + Math.floor(Math.random() * 1000000)
|
||||||
}
|
}
|
||||||
createArticle({ article })
|
createArticle({ article })
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { Icon } from '../_shared/Icon'
|
||||||
import styles from './Card.module.scss'
|
import styles from './Card.module.scss'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import CardTopic from './CardTopic'
|
import { CardTopic } from './CardTopic'
|
||||||
import RatingControl from '../Article/RatingControl'
|
import { RatingControl } from '../Article/RatingControl'
|
||||||
|
|
||||||
interface ArticleCardProps {
|
interface ArticleCardProps {
|
||||||
settings?: {
|
settings?: {
|
||||||
|
@ -107,7 +107,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
<Show when={!props.settings?.isGroup}>
|
<Show when={!props.settings?.isGroup}>
|
||||||
<CardTopic
|
<CardTopic
|
||||||
title={
|
title={
|
||||||
locale() === 'ru' && mainTopic.title ? mainTopic.title : mainTopic.slug.replace('-', ' ')
|
locale() === 'ru' && mainTopic.title ? mainTopic.title : mainTopic?.slug?.replace('-', ' ')
|
||||||
}
|
}
|
||||||
slug={mainTopic.slug}
|
slug={mainTopic.slug}
|
||||||
isFloorImportant={props.settings?.isFloorImportant}
|
isFloorImportant={props.settings?.isFloorImportant}
|
||||||
|
@ -135,9 +135,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
<For each={authors}>
|
<For each={authors}>
|
||||||
{(author, index) => {
|
{(author, index) => {
|
||||||
const name =
|
const name =
|
||||||
author.name === 'Дискурс' && locale() !== 'ru'
|
author.name === 'Дискурс' && locale() !== 'ru' ? 'Discours' : translit(author.name)
|
||||||
? 'Discours'
|
|
||||||
: translit(author.name || '', locale() || 'ru')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -6,7 +6,7 @@ interface CardTopicProps {
|
||||||
isFloorImportant?: boolean
|
isFloorImportant?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (props: CardTopicProps) => {
|
export const CardTopic = (props: CardTopicProps) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={style.shoutTopic}
|
class={style.shoutTopic}
|
||||||
|
|
|
@ -20,7 +20,7 @@ type Props = {
|
||||||
const CreateModalContent = (props: Props) => {
|
const CreateModalContent = (props: Props) => {
|
||||||
const inviteUsers: inviteUser[] = props.users.map((user) => ({ ...user, selected: false }))
|
const inviteUsers: inviteUser[] = props.users.map((user) => ({ ...user, selected: false }))
|
||||||
const [theme, setTheme] = createSignal<string>('')
|
const [theme, setTheme] = createSignal<string>('')
|
||||||
const [slugs, setSlugs] = createSignal<string[]>([])
|
const [slugs, setSlugs] = createSignal<number[]>([])
|
||||||
const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers)
|
const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers)
|
||||||
let textInput: HTMLInputElement
|
let textInput: HTMLInputElement
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ const CreateModalContent = (props: Props) => {
|
||||||
return user.selected === true
|
return user.selected === true
|
||||||
})
|
})
|
||||||
.map((user) => {
|
.map((user) => {
|
||||||
return user['slug']
|
return user.id
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
if (slugs().length > 2 && theme().length === 0) {
|
if (slugs().length > 2 && theme().length === 0) {
|
||||||
|
|
|
@ -61,14 +61,14 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
const newValidationErrors: ValidationErrors = {}
|
const newValidationErrors: ValidationErrors = {}
|
||||||
|
|
||||||
const clearName = name().trim()
|
const cleanName = name().trim()
|
||||||
const clearEmail = email().trim()
|
const cleanEmail = email().trim()
|
||||||
|
|
||||||
if (!clearName) {
|
if (!cleanName) {
|
||||||
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 (!clearEmail) {
|
if (!cleanEmail) {
|
||||||
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')
|
||||||
|
@ -80,7 +80,7 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
setValidationErrors(newValidationErrors)
|
setValidationErrors(newValidationErrors)
|
||||||
|
|
||||||
const emailCheckResult = await checkEmail(clearEmail)
|
const emailCheckResult = await checkEmail(cleanEmail)
|
||||||
|
|
||||||
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
|
||||||
|
|
||||||
|
@ -92,8 +92,8 @@ export const RegisterForm = () => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await register({
|
await register({
|
||||||
name: clearName,
|
name: cleanName,
|
||||||
email: clearEmail,
|
email: cleanEmail,
|
||||||
password: password()
|
password: password()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -123,13 +123,13 @@ export const RegisterForm = () => {
|
||||||
</Show>
|
</Show>
|
||||||
<div class="pretty-form__item">
|
<div class="pretty-form__item">
|
||||||
<input
|
<input
|
||||||
name="name"
|
name="fullName"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('Full name')}
|
placeholder={t('Full name')}
|
||||||
autocomplete=""
|
autocomplete=""
|
||||||
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
onInput={(event) => handleNameInput(event.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<label for="name">{t('Full name')}</label>
|
<label for="fullName">{t('Full name')}</label>
|
||||||
</div>
|
</div>
|
||||||
<Show when={validationErrors().name}>
|
<Show when={validationErrors().name}>
|
||||||
<div class={styles.validationError}>{validationErrors().name}</div>
|
<div class={styles.validationError}>{validationErrors().name}</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { clsx } from 'clsx'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { Icon } from '../_shared/Icon'
|
import { Icon } from '../_shared/Icon'
|
||||||
import { createSignal, Show } from 'solid-js'
|
import { createEffect, createSignal, Show } from 'solid-js'
|
||||||
import Notifications from './Notifications'
|
import Notifications from './Notifications'
|
||||||
import { ProfilePopup } from './ProfilePopup'
|
import { ProfilePopup } from './ProfilePopup'
|
||||||
import Userpic from '../Author/Userpic'
|
import Userpic from '../Author/Userpic'
|
||||||
|
@ -38,7 +38,7 @@ export const HeaderAuth = (props: HeaderAuthProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowOnlyOnClient>
|
<ShowOnlyOnClient>
|
||||||
<Show when={!session.loading}>
|
<Show when={session.state !== 'pending'}>
|
||||||
<div class={styles.usernav}>
|
<div class={styles.usernav}>
|
||||||
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
<div class={clsx(styles.userControl, styles.userControl, 'col')}>
|
||||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||||
|
|
|
@ -23,15 +23,16 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1em;
|
top: 1em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 0.8em;
|
height: 18px;
|
||||||
|
width: 16px;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
width: 0.8em;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
display: block;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@
|
||||||
@media (min-width: 800px) and (max-width: 991px) {
|
@media (min-width: 800px) and (max-width: 991px) {
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
right: 12px;
|
right: 12px;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
|
|
|
@ -116,3 +116,7 @@
|
||||||
.buttonCompact {
|
.buttonCompact {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.isSubscribing {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { capitalize } from '../../utils'
|
import { capitalize } from '../../utils'
|
||||||
import styles from './Card.module.scss'
|
import styles from './Card.module.scss'
|
||||||
import { createMemo, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, 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 { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
|
@ -9,6 +9,7 @@ import { getLogger } from '../../utils/logger'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { useSession } from '../../context/session'
|
import { useSession } from '../../context/session'
|
||||||
import { StatMetrics } from '../_shared/StatMetrics'
|
import { StatMetrics } from '../_shared/StatMetrics'
|
||||||
|
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
|
||||||
|
|
||||||
const log = getLogger('TopicCard')
|
const log = getLogger('TopicCard')
|
||||||
|
|
||||||
|
@ -25,7 +26,12 @@ interface TopicProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicCard = (props: TopicProps) => {
|
export const TopicCard = (props: TopicProps) => {
|
||||||
const { session } = useSession()
|
const {
|
||||||
|
session,
|
||||||
|
actions: { loadSession }
|
||||||
|
} = useSession()
|
||||||
|
|
||||||
|
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||||
|
|
||||||
const subscribed = createMemo(() => {
|
const subscribed = createMemo(() => {
|
||||||
if (!session()?.user?.slug || !session()?.news?.topics) {
|
if (!session()?.user?.slug || !session()?.news?.topics) {
|
||||||
|
@ -35,14 +41,17 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
return session()?.news.topics.includes(props.topic.slug)
|
return session()?.news.topics.includes(props.topic.slug)
|
||||||
})
|
})
|
||||||
|
|
||||||
// FIXME use store actions
|
|
||||||
const subscribe = async (really = true) => {
|
const subscribe = async (really = true) => {
|
||||||
if (really) {
|
setIsSubscribing(true)
|
||||||
follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
|
||||||
} else {
|
await (really
|
||||||
unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||||
}
|
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
|
||||||
|
|
||||||
|
await loadSession()
|
||||||
|
setIsSubscribing(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={styles.topic}
|
class={styles.topic}
|
||||||
|
@ -79,32 +88,30 @@ export const TopicCard = (props: TopicProps) => {
|
||||||
class={styles.controlContainer}
|
class={styles.controlContainer}
|
||||||
classList={{ 'col-md-3': !props.compact && !props.subscribeButtonBottom }}
|
classList={{ 'col-md-3': !props.compact && !props.subscribeButtonBottom }}
|
||||||
>
|
>
|
||||||
<Show
|
<ShowOnlyOnClient>
|
||||||
when={subscribed()}
|
<Show when={session.state !== 'pending'}>
|
||||||
fallback={
|
|
||||||
<button
|
<button
|
||||||
onClick={() => subscribe(true)}
|
onClick={() => subscribe(!subscribed())}
|
||||||
class="button--light button--subscribe-topic"
|
class="button--light button--subscribe-topic"
|
||||||
classList={{
|
classList={{
|
||||||
[styles.buttonCompact]: props.compact
|
[styles.buttonCompact]: props.compact,
|
||||||
|
[styles.isSubscribing]: isSubscribing()
|
||||||
}}
|
}}
|
||||||
|
disabled={isSubscribing()}
|
||||||
>
|
>
|
||||||
<Show when={props.iconButton}>+</Show>
|
<Show when={props.iconButton}>
|
||||||
<Show when={!props.iconButton}>{t('Follow')}</Show>
|
<Show when={subscribed()} fallback="+">
|
||||||
</button>
|
-
|
||||||
}
|
</Show>
|
||||||
>
|
</Show>
|
||||||
<button
|
<Show when={!props.iconButton}>
|
||||||
onClick={() => subscribe(false)}
|
<Show when={subscribed()} fallback={t('Follow')}>
|
||||||
class="button--light button--subscribe-topic"
|
{t('Unfollow')}
|
||||||
classList={{
|
</Show>
|
||||||
[styles.buttonCompact]: props.compact
|
</Show>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Show when={props.iconButton}>-</Show>
|
|
||||||
<Show when={!props.iconButton}>{t('Unfollow')}</Show>
|
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
</ShowOnlyOnClient>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
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 { AuthorsSortBy, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
|
import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
|
||||||
import { useRouter } from '../../stores/router'
|
import { useRouter } from '../../stores/router'
|
||||||
import { AuthorCard } from '../Author/Card'
|
import { AuthorCard } from '../Author/Card'
|
||||||
import { clsx } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
|
@ -22,36 +22,38 @@ type AllAuthorsViewProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
const ALPHABET = [...'@АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ']
|
const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@']
|
||||||
|
|
||||||
export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
const [limit, setLimit] = createSignal(PAGE_SIZE)
|
||||||
const { searchParams, changeSearchParam } = useRouter()
|
const { searchParams, changeSearchParam } = useRouter<AllAuthorsPageSearchParams>()
|
||||||
const [filterResults, setFilterResults] = createSignal<Author[]>([])
|
|
||||||
const { sortedAuthors } = useAuthorsStore({
|
const { sortedAuthors } = useAuthorsStore({
|
||||||
authors: props.authors,
|
authors: props.authors,
|
||||||
sortBy: (searchParams().by || 'shouts') as AuthorsSortBy
|
sortBy: searchParams().by || 'shouts'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
|
|
||||||
const { session } = useSession()
|
const { session } = useSession()
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!searchParams().by) {
|
if (!searchParams().by) {
|
||||||
setAuthorsSort('name')
|
changeSearchParam('by', 'shouts')
|
||||||
changeSearchParam('by', 'name')
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setAuthorsSort((searchParams().by || 'shouts') as AuthorsSortBy)
|
setAuthorsSort(searchParams().by || 'shouts')
|
||||||
setFilterResults(sortedAuthors())
|
|
||||||
setLimit(PAGE_SIZE)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||||
return sortedAuthors().reduce((acc, author) => {
|
return sortedAuthors().reduce((acc, author) => {
|
||||||
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
|
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase()
|
||||||
if (!/[А-я]/i.test(letter) && locale() === 'ru') letter = '@'
|
|
||||||
|
if (/[^ËА-яё]/.test(letter) && locale() === 'ru') letter = '@'
|
||||||
|
|
||||||
if (!acc[letter]) acc[letter] = []
|
if (!acc[letter]) acc[letter] = []
|
||||||
|
|
||||||
acc[letter].push(author)
|
acc[letter].push(author)
|
||||||
return acc
|
return acc
|
||||||
}, {} as { [letter: string]: Author[] })
|
}, {} as { [letter: string]: Author[] })
|
||||||
|
@ -60,41 +62,34 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
const sortedKeys = createMemo<string[]>(() => {
|
const sortedKeys = createMemo<string[]>(() => {
|
||||||
const keys = Object.keys(byLetter())
|
const keys = Object.keys(byLetter())
|
||||||
keys.sort()
|
keys.sort()
|
||||||
|
keys.push(keys.shift())
|
||||||
return keys
|
return keys
|
||||||
})
|
})
|
||||||
|
|
||||||
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
|
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
const filteredAuthors = createMemo(() => {
|
||||||
const filterAuthors = (value) => {
|
let q = searchQuery().toLowerCase()
|
||||||
/* very stupid filter by string algorithm with no deps */
|
|
||||||
let q = value.toLowerCase()
|
|
||||||
if (q.length > 0) {
|
|
||||||
setFilterResults([])
|
|
||||||
if (locale() === 'ru') q = translit(q, 'ru')
|
|
||||||
const aaa: Author[] = sortedAuthors()
|
|
||||||
sortedAuthors().forEach((author) => {
|
|
||||||
let flag = false
|
|
||||||
author.slug.split('-').forEach((w) => {
|
|
||||||
if (w.startsWith(q)) flag = true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!flag) {
|
if (q.length === 0) {
|
||||||
let wrds: string = author.name.toLowerCase()
|
return sortedAuthors()
|
||||||
if (locale() === 'ru') wrds = translit(wrds, 'ru')
|
|
||||||
wrds.split(' ').forEach((w: string) => {
|
|
||||||
if (w.startsWith(q)) flag = true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flag && aaa.includes(author)) {
|
if (locale() === 'ru') q = translit(q)
|
||||||
const idx = aaa.indexOf(author)
|
|
||||||
aaa.splice(idx, 1)
|
return sortedAuthors().filter((author) => {
|
||||||
|
if (author.slug.split('-').some((w) => w.startsWith(q))) {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let name = author.name.toLowerCase()
|
||||||
|
if (locale() === 'ru') {
|
||||||
|
name = translit(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return name.split(' ').some((word) => word.startsWith(q))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
setFilterResults(aaa)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||||
const AllAuthorsHead = () => (
|
const AllAuthorsHead = () => (
|
||||||
|
@ -104,7 +99,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
||||||
|
|
||||||
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
||||||
<li classList={{ selected: searchParams().by === 'shouts' }}>
|
<li classList={{ selected: !searchParams().by || searchParams().by === 'shouts' }}>
|
||||||
<a href="/authors?by=shouts">{t('By shouts')}</a>
|
<a href="/authors?by=shouts">{t('By shouts')}</a>
|
||||||
</li>
|
</li>
|
||||||
<li classList={{ selected: searchParams().by === 'followers' }}>
|
<li classList={{ selected: searchParams().by === 'followers' }}>
|
||||||
|
@ -115,7 +110,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
</li>
|
</li>
|
||||||
<Show when={searchParams().by !== 'name'}>
|
<Show when={searchParams().by !== 'name'}>
|
||||||
<li class="view-switcher__search">
|
<li class="view-switcher__search">
|
||||||
<SearchField onChange={filterAuthors} />
|
<SearchField onChange={(value) => setSearchQuery(value)} />
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
</Show>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -139,7 +134,10 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
<Show when={letter in byLetter()} fallback={letter}>
|
<Show when={letter in byLetter()} fallback={letter}>
|
||||||
<a
|
<a
|
||||||
href={`/authors?by=name#letter-${index()}`}
|
href={`/authors?by=name#letter-${index()}`}
|
||||||
onClick={() => scrollHandler(`letter-${index()}`)}
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollHandler(`letter-${index()}`)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{letter}
|
{letter}
|
||||||
</a>
|
</a>
|
||||||
|
@ -152,9 +150,9 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<For each={sortedKeys()}>
|
<For each={sortedKeys()}>
|
||||||
{(letter, index) => (
|
{(letter) => (
|
||||||
<div class={clsx(styles.group, 'group')}>
|
<div class={clsx(styles.group, 'group')}>
|
||||||
<h2 id={`letter-${index()}`}>{letter}</h2>
|
<h2 id={`letter-${ALPHABET.indexOf(letter)}`}>{letter}</h2>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-10">
|
<div class="col-lg-10">
|
||||||
|
@ -178,8 +176,8 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={searchParams().by && searchParams().by !== 'title'}>
|
<Show when={searchParams().by && searchParams().by !== 'name'}>
|
||||||
<For each={filterResults().slice(0, limit())}>
|
<For each={filteredAuthors().slice(0, limit())}>
|
||||||
{(author) => (
|
{(author) => (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-10 col-xl-9">
|
<div class="col-lg-10 col-xl-9">
|
||||||
|
@ -197,7 +195,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={sortedAuthors().length > limit() && searchParams().by !== 'name'}>
|
<Show when={filteredAuthors().length > limit() && searchParams().by !== 'name'}>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}>
|
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10')}>
|
||||||
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||||||
|
|
|
@ -22,7 +22,7 @@ type AllTopicsViewProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 20
|
const PAGE_SIZE = 20
|
||||||
const ALPHABET = [...'#АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ']
|
const ALPHABET = [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ#']
|
||||||
|
|
||||||
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
const { searchParams, changeSearchParam } = useRouter<AllTopicsPageSearchParams>()
|
||||||
|
@ -37,20 +37,18 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (!searchParams().by) {
|
if (!searchParams().by) {
|
||||||
setTopicsSort('shouts')
|
|
||||||
changeSearchParam('by', 'shouts')
|
changeSearchParam('by', 'shouts')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setTopicsSort(searchParams().by || 'shouts')
|
setTopicsSort(searchParams().by || 'shouts')
|
||||||
setFilterResults(sortedTopics())
|
|
||||||
setLimit(PAGE_SIZE)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
const byLetter = createMemo<{ [letter: string]: Topic[] }>(() => {
|
||||||
return sortedTopics().reduce((acc, topic) => {
|
return sortedTopics().reduce((acc, topic) => {
|
||||||
let letter = topic.title[0].toUpperCase()
|
let letter = topic.title[0].toUpperCase()
|
||||||
if (!/[А-я]/i.test(letter) && locale() === 'ru') letter = '#'
|
if (/[^ËА-яё]/.test(letter) && locale() === 'ru') letter = '#'
|
||||||
if (!acc[letter]) acc[letter] = []
|
if (!acc[letter]) acc[letter] = []
|
||||||
acc[letter].push(topic)
|
acc[letter].push(topic)
|
||||||
return acc
|
return acc
|
||||||
|
@ -60,6 +58,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
const sortedKeys = createMemo<string[]>(() => {
|
const sortedKeys = createMemo<string[]>(() => {
|
||||||
const keys = Object.keys(byLetter())
|
const keys = Object.keys(byLetter())
|
||||||
keys.sort()
|
keys.sort()
|
||||||
|
keys.push(keys.shift())
|
||||||
return keys
|
return keys
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -67,37 +66,32 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
|
|
||||||
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
|
||||||
|
|
||||||
const [filterResults, setFilterResults] = createSignal<Topic[]>([])
|
const [searchQuery, setSearchQuery] = createSignal('')
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
|
||||||
const filterTopics = (value) => {
|
const filteredResults = createMemo(() => {
|
||||||
/* very stupid filter by string algorithm with no deps */
|
/* very stupid filter by string algorithm with no deps */
|
||||||
let q = value.toLowerCase()
|
let q = searchQuery().toLowerCase()
|
||||||
if (q.length > 0) {
|
if (q.length === 0) {
|
||||||
setFilterResults([])
|
return sortedTopics()
|
||||||
if (locale() === 'ru') q = translit(q, 'ru')
|
|
||||||
const ttt: Topic[] = sortedTopics()
|
|
||||||
sortedTopics().forEach((topic) => {
|
|
||||||
let flag = false
|
|
||||||
topic.slug.split('-').forEach((w) => {
|
|
||||||
if (w.startsWith(q)) flag = true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!flag) {
|
|
||||||
let wrds: string = topic.title.toLowerCase()
|
|
||||||
if (locale() === 'ru') wrds = translit(wrds, 'ru')
|
|
||||||
wrds.split(' ').forEach((w: string) => {
|
|
||||||
if (w.startsWith(q)) flag = true
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flag && ttt.includes(topic)) {
|
if (locale() === 'ru') {
|
||||||
const idx = ttt.indexOf(topic)
|
q = translit(q)
|
||||||
ttt.splice(idx, 1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return sortedTopics().filter((topic) => {
|
||||||
|
if (topic.slug.split('-').some((w) => w.startsWith(q))) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = topic.title.toLowerCase()
|
||||||
|
if (locale() === 'ru') {
|
||||||
|
title = translit(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return title.split(' ').some((word) => word.startsWith(q))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
setFilterResults(ttt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AllTopicsHead = () => (
|
const AllTopicsHead = () => (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -117,7 +111,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
</li>
|
</li>
|
||||||
<Show when={searchParams().by !== 'title'}>
|
<Show when={searchParams().by !== 'title'}>
|
||||||
<li class="view-switcher__search">
|
<li class="view-switcher__search">
|
||||||
<SearchField onChange={filterTopics} />
|
<SearchField onChange={(value) => setSearchQuery(value)} />
|
||||||
</li>
|
</li>
|
||||||
</Show>
|
</Show>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -130,7 +124,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
<div class="shift-content">
|
<div class="shift-content">
|
||||||
<AllTopicsHead />
|
<AllTopicsHead />
|
||||||
|
|
||||||
<Show when={filterResults().length > 0}>
|
<Show when={filteredResults().length > 0}>
|
||||||
<Show when={searchParams().by === 'title'}>
|
<Show when={searchParams().by === 'title'}>
|
||||||
<div class="col-lg-10 col-xl-9">
|
<div class="col-lg-10 col-xl-9">
|
||||||
<ul class={clsx('nodash', styles.alphabet)}>
|
<ul class={clsx('nodash', styles.alphabet)}>
|
||||||
|
@ -140,7 +134,10 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
<Show when={letter in byLetter()} fallback={letter}>
|
<Show when={letter in byLetter()} fallback={letter}>
|
||||||
<a
|
<a
|
||||||
href={`/topics?by=title#letter-${index()}`}
|
href={`/topics?by=title#letter-${index()}`}
|
||||||
onClick={() => scrollHandler(`letter-${index()}`)}
|
onClick={(event) => {
|
||||||
|
event.preventDefault()
|
||||||
|
scrollHandler(`letter-${index()}`)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{letter}
|
{letter}
|
||||||
</a>
|
</a>
|
||||||
|
@ -152,9 +149,9 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<For each={sortedKeys()}>
|
<For each={sortedKeys()}>
|
||||||
{(letter, index) => (
|
{(letter) => (
|
||||||
<div class={clsx(styles.group, 'group')}>
|
<div class={clsx(styles.group, 'group')}>
|
||||||
<h2 id={`letter-${index()}`}>{letter}</h2>
|
<h2 id={`letter-${ALPHABET.indexOf(letter)}`}>{letter}</h2>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-10">
|
<div class="col-lg-10">
|
||||||
|
@ -177,7 +174,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={searchParams().by && searchParams().by !== 'title'}>
|
<Show when={searchParams().by && searchParams().by !== 'title'}>
|
||||||
<For each={filterResults().slice(0, limit())}>
|
<For each={filteredResults().slice(0, limit())}>
|
||||||
{(topic) => (
|
{(topic) => (
|
||||||
<>
|
<>
|
||||||
<TopicCard
|
<TopicCard
|
||||||
|
@ -192,7 +189,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={sortedTopics().length > limit() && searchParams().by !== 'title'}>
|
<Show when={filteredResults().length > limit() && searchParams().by !== 'title'}>
|
||||||
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10 offset-md-1')}>
|
<div class={clsx(styles.loadMoreContainer, 'col-12 col-md-10 offset-md-1')}>
|
||||||
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||||||
{t('Load more')}
|
{t('Load more')}
|
||||||
|
|
|
@ -13,7 +13,12 @@ import Group from '../Feed/Group'
|
||||||
import type { Shout, Topic } from '../../graphql/types.gen'
|
import type { Shout, Topic } from '../../graphql/types.gen'
|
||||||
import { t } from '../../utils/intl'
|
import { t } from '../../utils/intl'
|
||||||
import { useTopicsStore } from '../../stores/zine/topics'
|
import { useTopicsStore } from '../../stores/zine/topics'
|
||||||
import { loadShouts, useArticlesStore } from '../../stores/zine/articles'
|
import {
|
||||||
|
loadShouts,
|
||||||
|
loadTopArticles,
|
||||||
|
loadTopMonthArticles,
|
||||||
|
useArticlesStore
|
||||||
|
} from '../../stores/zine/articles'
|
||||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||||
import { locale } from '../../stores/ui'
|
import { locale } from '../../stores/ui'
|
||||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||||
|
@ -48,6 +53,8 @@ export const HomeView = (props: HomeProps) => {
|
||||||
const { topAuthors } = useTopAuthorsStore()
|
const { topAuthors } = useTopAuthorsStore()
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
loadTopArticles()
|
||||||
|
loadTopMonthArticles()
|
||||||
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
|
||||||
const { hasMore } = await loadShouts({
|
const { hasMore } = await loadShouts({
|
||||||
filters: { visibility: 'public' },
|
filters: { visibility: 'public' },
|
||||||
|
|
|
@ -149,7 +149,8 @@ export const TopicView = (props: TopicProps) => {
|
||||||
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
<Row2 articles={sortedArticles().slice(13, 15)} isEqual={true} />
|
||||||
<Row1 article={sortedArticles()[15]} />
|
<Row1 article={sortedArticles()[15]} />
|
||||||
|
|
||||||
<Slider slidesPerView={3} title={title()}>
|
<Show when={sortedArticles().length > 15}>
|
||||||
|
<Slider slidesPerView={3}>
|
||||||
<For each={sortedArticles().slice(16, 22)}>
|
<For each={sortedArticles().slice(16, 22)}>
|
||||||
{(a: Shout) => (
|
{(a: Shout) => (
|
||||||
<ArticleCard
|
<ArticleCard
|
||||||
|
@ -167,6 +168,7 @@ export const TopicView = (props: TopicProps) => {
|
||||||
|
|
||||||
<Row3 articles={sortedArticles().slice(23, 26)} />
|
<Row3 articles={sortedArticles().slice(23, 26)} />
|
||||||
<Row2 articles={sortedArticles().slice(26, 28)} />
|
<Row2 articles={sortedArticles().slice(26, 28)} />
|
||||||
|
</Show>
|
||||||
|
|
||||||
<For each={pages()}>
|
<For each={pages()}>
|
||||||
{(page) => (
|
{(page) => (
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { createStore } from 'solid-js/store'
|
||||||
type InboxContextType = {
|
type InboxContextType = {
|
||||||
chatEntities: { [chatId: string]: Message[] }
|
chatEntities: { [chatId: string]: Message[] }
|
||||||
actions: {
|
actions: {
|
||||||
createChat: (members: string[], title: string) => Promise<void>
|
createChat: (members: number[], title: string) => Promise<void>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export function useInbox() {
|
||||||
export const InboxProvider = (props: { children: JSX.Element }) => {
|
export const InboxProvider = (props: { children: JSX.Element }) => {
|
||||||
const [chatEntities, setChatEntities] = createStore({})
|
const [chatEntities, setChatEntities] = createStore({})
|
||||||
|
|
||||||
const createChat = async (members: string[], title: string) => {
|
const createChat = async (members: number[], title: string) => {
|
||||||
const chat = await apiClient.createChat({ members, title })
|
const chat = await apiClient.createChat({ members, title })
|
||||||
setChatEntities((s) => {
|
setChatEntities((s) => {
|
||||||
s[chat.id] = chat
|
s[chat.id] = chat
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import type { Accessor, InitializedResource, JSX } from 'solid-js'
|
import type { Accessor, JSX, Resource } from 'solid-js'
|
||||||
import { createContext, createMemo, createResource, onMount, useContext } from 'solid-js'
|
import { createContext, createMemo, createResource, onMount, useContext } from 'solid-js'
|
||||||
import type { AuthResult } from '../graphql/types.gen'
|
import type { AuthResult } from '../graphql/types.gen'
|
||||||
import { apiClient } from '../utils/apiClient'
|
import { apiClient } from '../utils/apiClient'
|
||||||
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
import { resetToken, setToken } from '../graphql/privateGraphQLClient'
|
||||||
|
|
||||||
type SessionContextType = {
|
type SessionContextType = {
|
||||||
session: InitializedResource<AuthResult>
|
session: Resource<AuthResult>
|
||||||
userSlug: Accessor<string>
|
userSlug: Accessor<string>
|
||||||
isAuthenticated: Accessor<boolean>
|
isAuthenticated: Accessor<boolean>
|
||||||
actions: {
|
actions: {
|
||||||
getSession: () => AuthResult | Promise<AuthResult>
|
loadSession: () => AuthResult | Promise<AuthResult>
|
||||||
signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
|
signIn: ({ email, password }: { email: string; password: string }) => Promise<void>
|
||||||
signOut: () => Promise<void>
|
signOut: () => Promise<void>
|
||||||
confirmEmail: (token: string) => Promise<void>
|
confirmEmail: (token: string) => Promise<void>
|
||||||
|
@ -27,7 +27,7 @@ const getSession = async (): Promise<AuthResult> => {
|
||||||
setToken(authResult.token)
|
setToken(authResult.token)
|
||||||
return authResult
|
return authResult
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('renewSession error:', error)
|
console.error('getSession error:', error)
|
||||||
resetToken()
|
resetToken()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -38,10 +38,7 @@ export function useSession() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SessionProvider = (props: { children: JSX.Element }) => {
|
export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
const [session, { refetch: refetchSession, mutate }] = createResource<AuthResult>(getSession, {
|
const [session, { refetch: loadSession, mutate }] = createResource<AuthResult>(getSession)
|
||||||
ssrLoadFrom: 'initial',
|
|
||||||
initialValue: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const userSlug = createMemo(() => session()?.user?.slug)
|
const userSlug = createMemo(() => session()?.user?.slug)
|
||||||
|
|
||||||
|
@ -68,7 +65,7 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
getSession: refetchSession,
|
loadSession,
|
||||||
signIn,
|
signIn,
|
||||||
signOut,
|
signOut,
|
||||||
confirmEmail
|
confirmEmail
|
||||||
|
@ -77,7 +74,7 @@ export const SessionProvider = (props: { children: JSX.Element }) => {
|
||||||
const value: SessionContextType = { session, userSlug, isAuthenticated, actions }
|
const value: SessionContextType = { session, userSlug, isAuthenticated, actions }
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
refetchSession()
|
loadSession()
|
||||||
})
|
})
|
||||||
|
|
||||||
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
|
return <SessionContext.Provider value={value}>{props.children}</SessionContext.Provider>
|
||||||
|
|
|
@ -11,12 +11,12 @@ export default gql`
|
||||||
subtitle
|
subtitle
|
||||||
body
|
body
|
||||||
topics {
|
topics {
|
||||||
_id: slug
|
id
|
||||||
title
|
title
|
||||||
slug
|
slug
|
||||||
}
|
}
|
||||||
authors {
|
authors {
|
||||||
_id: slug
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
|
|
@ -12,13 +12,13 @@ export default gql`
|
||||||
image
|
image
|
||||||
body
|
body
|
||||||
topics {
|
topics {
|
||||||
_id: slug
|
id
|
||||||
title
|
title
|
||||||
slug
|
slug
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
authors {
|
authors {
|
||||||
_id: slug
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation FollowQuery($what: String!, $slug: String!) {
|
mutation FollowMutation($what: FollowingEntity!, $slug: String!) {
|
||||||
follow(what: $what, slug: $slug) {
|
follow(what: $what, slug: $slug) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UnfollowQuery($what: String!, $slug: String!) {
|
mutation UnfollowMutation($what: FollowingEntity!, $slug: String!) {
|
||||||
unfollow(what: $what, slug: $slug) {
|
unfollow(what: $what, slug: $slug) {
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@ export const getToken = (): string => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setToken = (token: string) => {
|
export const setToken = (token: string) => {
|
||||||
|
if (!token) {
|
||||||
|
console.error('[privateGraphQLClient] setToken: token is null!')
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token)
|
localStorage.setItem(TOKEN_LOCAL_STORAGE_KEY, token)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,11 +32,12 @@ const options: ClientOptions = {
|
||||||
maskTypename: true,
|
maskTypename: true,
|
||||||
requestPolicy: 'cache-and-network',
|
requestPolicy: 'cache-and-network',
|
||||||
fetchOptions: () => {
|
fetchOptions: () => {
|
||||||
// пока источником правды для значения токена будет локальное хранилище
|
// localStorage is the source of truth for now
|
||||||
// меняем через setToken, например при получении значения с сервера
|
// to change token call setToken, for example after login
|
||||||
// скорее всего придумаем что-нибудь получше со временем
|
|
||||||
const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
|
const token = localStorage.getItem(TOKEN_LOCAL_STORAGE_KEY)
|
||||||
if (token === null) alert('token is null')
|
if (!token) {
|
||||||
|
console.error('[privateGraphQLClient] fetchOptions: token is null!')
|
||||||
|
}
|
||||||
const headers = { Authorization: token }
|
const headers = { Authorization: token }
|
||||||
return { headers }
|
return { headers }
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default gql`
|
||||||
# community
|
# community
|
||||||
mainTopic
|
mainTopic
|
||||||
topics {
|
topics {
|
||||||
|
id
|
||||||
title
|
title
|
||||||
body
|
body
|
||||||
slug
|
slug
|
||||||
|
@ -25,7 +26,7 @@ export default gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authors {
|
authors {
|
||||||
_id: slug
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
|
|
@ -12,6 +12,7 @@ export default gql`
|
||||||
# community
|
# community
|
||||||
mainTopic
|
mainTopic
|
||||||
topics {
|
topics {
|
||||||
|
id
|
||||||
title
|
title
|
||||||
body
|
body
|
||||||
slug
|
slug
|
||||||
|
@ -23,7 +24,7 @@ export default gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authors {
|
authors {
|
||||||
_id: slug
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
|
|
@ -14,6 +14,7 @@ export default gql`
|
||||||
# community
|
# community
|
||||||
mainTopic
|
mainTopic
|
||||||
topics {
|
topics {
|
||||||
|
id
|
||||||
title
|
title
|
||||||
body
|
body
|
||||||
slug
|
slug
|
||||||
|
@ -25,7 +26,7 @@ export default gql`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
authors {
|
authors {
|
||||||
_id: slug
|
id
|
||||||
name
|
name
|
||||||
slug
|
slug
|
||||||
userpic
|
userpic
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default gql`
|
||||||
query GetCollabsQuery {
|
query GetCollabsQuery {
|
||||||
getCollabs {
|
getCollabs {
|
||||||
authors {
|
authors {
|
||||||
|
id
|
||||||
slug
|
slug
|
||||||
name
|
name
|
||||||
pic
|
pic
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
query TopicsRandomQuery {
|
query TopicsRandomQuery($amount: Int) {
|
||||||
topicsRandom {
|
topicsRandom(amount: $amount) {
|
||||||
_id: slug
|
_id: slug
|
||||||
title
|
title
|
||||||
body
|
body
|
||||||
|
|
|
@ -84,10 +84,10 @@ export type ChatMember = {
|
||||||
|
|
||||||
export type Collab = {
|
export type Collab = {
|
||||||
authors: Array<Maybe<Scalars['String']>>
|
authors: Array<Maybe<Scalars['String']>>
|
||||||
body?: Maybe<Scalars['String']>
|
chat?: Maybe<Chat>
|
||||||
createdAt: Scalars['DateTime']
|
createdAt: Scalars['Int']
|
||||||
invites?: Maybe<Array<Maybe<Scalars['String']>>>
|
invites?: Maybe<Array<Maybe<Scalars['String']>>>
|
||||||
title?: Maybe<Scalars['String']>
|
shout?: Maybe<Shout>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Collection = {
|
export type Collection = {
|
||||||
|
@ -164,6 +164,7 @@ export type MessagesBy = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
|
acceptCoauthor: Result
|
||||||
confirmEmail: AuthResult
|
confirmEmail: AuthResult
|
||||||
createChat: Result
|
createChat: Result
|
||||||
createMessage: Result
|
createMessage: Result
|
||||||
|
@ -177,11 +178,11 @@ export type Mutation = {
|
||||||
destroyTopic: Result
|
destroyTopic: Result
|
||||||
follow: Result
|
follow: Result
|
||||||
getSession: AuthResult
|
getSession: AuthResult
|
||||||
inviteAuthor: Result
|
inviteCoauthor: Result
|
||||||
markAsRead: Result
|
markAsRead: Result
|
||||||
rateUser: Result
|
rateUser: Result
|
||||||
registerUser: AuthResult
|
registerUser: AuthResult
|
||||||
removeAuthor: Result
|
removeCoauthor: Result
|
||||||
sendLink: Result
|
sendLink: Result
|
||||||
unfollow: Result
|
unfollow: Result
|
||||||
updateChat: Result
|
updateChat: Result
|
||||||
|
@ -193,19 +194,23 @@ export type Mutation = {
|
||||||
updateTopic: Result
|
updateTopic: Result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MutationAcceptCoauthorArgs = {
|
||||||
|
shout: Scalars['Int']
|
||||||
|
}
|
||||||
|
|
||||||
export type MutationConfirmEmailArgs = {
|
export type MutationConfirmEmailArgs = {
|
||||||
token: Scalars['String']
|
token: Scalars['String']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationCreateChatArgs = {
|
export type MutationCreateChatArgs = {
|
||||||
members: Array<InputMaybe<Scalars['String']>>
|
members: Array<InputMaybe<Scalars['Int']>>
|
||||||
title?: InputMaybe<Scalars['String']>
|
title?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationCreateMessageArgs = {
|
export type MutationCreateMessageArgs = {
|
||||||
body: Scalars['String']
|
body: Scalars['String']
|
||||||
chat: Scalars['String']
|
chat: Scalars['String']
|
||||||
replyTo?: InputMaybe<Scalars['String']>
|
replyTo?: InputMaybe<Scalars['Int']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationCreateReactionArgs = {
|
export type MutationCreateReactionArgs = {
|
||||||
|
@ -213,7 +218,7 @@ export type MutationCreateReactionArgs = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationCreateShoutArgs = {
|
export type MutationCreateShoutArgs = {
|
||||||
input: ShoutInput
|
inp: ShoutInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationCreateTopicArgs = {
|
export type MutationCreateTopicArgs = {
|
||||||
|
@ -246,9 +251,9 @@ export type MutationFollowArgs = {
|
||||||
what: FollowingEntity
|
what: FollowingEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationInviteAuthorArgs = {
|
export type MutationInviteCoauthorArgs = {
|
||||||
author: Scalars['String']
|
author: Scalars['String']
|
||||||
shout: Scalars['String']
|
shout: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationMarkAsReadArgs = {
|
export type MutationMarkAsReadArgs = {
|
||||||
|
@ -267,14 +272,15 @@ export type MutationRegisterUserArgs = {
|
||||||
password?: InputMaybe<Scalars['String']>
|
password?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationRemoveAuthorArgs = {
|
export type MutationRemoveCoauthorArgs = {
|
||||||
author: Scalars['String']
|
author: Scalars['String']
|
||||||
shout: Scalars['String']
|
shout: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationSendLinkArgs = {
|
export type MutationSendLinkArgs = {
|
||||||
email: Scalars['String']
|
email: Scalars['String']
|
||||||
lang?: InputMaybe<Scalars['String']>
|
lang?: InputMaybe<Scalars['String']>
|
||||||
|
template?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationUnfollowArgs = {
|
export type MutationUnfollowArgs = {
|
||||||
|
@ -301,7 +307,7 @@ export type MutationUpdateReactionArgs = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationUpdateShoutArgs = {
|
export type MutationUpdateShoutArgs = {
|
||||||
input: ShoutInput
|
inp: ShoutInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MutationUpdateTopicArgs = {
|
export type MutationUpdateTopicArgs = {
|
||||||
|
@ -320,14 +326,16 @@ export type Operation = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Permission = {
|
export type Permission = {
|
||||||
operation_id: Scalars['Int']
|
operation: Scalars['Int']
|
||||||
resource_id: Scalars['Int']
|
resource: Scalars['Int']
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ProfileInput = {
|
export type ProfileInput = {
|
||||||
|
about?: InputMaybe<Scalars['String']>
|
||||||
bio?: InputMaybe<Scalars['String']>
|
bio?: InputMaybe<Scalars['String']>
|
||||||
links?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
links?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
||||||
name?: InputMaybe<Scalars['String']>
|
name?: InputMaybe<Scalars['String']>
|
||||||
|
slug?: InputMaybe<Scalars['String']>
|
||||||
userpic?: InputMaybe<Scalars['String']>
|
userpic?: InputMaybe<Scalars['String']>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,7 +469,7 @@ export type Reaction = {
|
||||||
old_id?: Maybe<Scalars['String']>
|
old_id?: Maybe<Scalars['String']>
|
||||||
old_thread?: Maybe<Scalars['String']>
|
old_thread?: Maybe<Scalars['String']>
|
||||||
range?: Maybe<Scalars['String']>
|
range?: Maybe<Scalars['String']>
|
||||||
replyTo?: Maybe<Reaction>
|
replyTo?: Maybe<Scalars['Int']>
|
||||||
shout: Shout
|
shout: Shout
|
||||||
stat?: Maybe<Stat>
|
stat?: Maybe<Stat>
|
||||||
updatedAt?: Maybe<Scalars['DateTime']>
|
updatedAt?: Maybe<Scalars['DateTime']>
|
||||||
|
@ -576,13 +584,14 @@ export type Shout = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ShoutInput = {
|
export type ShoutInput = {
|
||||||
|
authors?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
||||||
body: Scalars['String']
|
body: Scalars['String']
|
||||||
community: Scalars['String']
|
community?: InputMaybe<Scalars['Int']>
|
||||||
mainTopic?: InputMaybe<Scalars['String']>
|
mainTopic?: InputMaybe<Scalars['String']>
|
||||||
slug: Scalars['String']
|
slug?: InputMaybe<Scalars['String']>
|
||||||
subtitle?: InputMaybe<Scalars['String']>
|
subtitle?: InputMaybe<Scalars['String']>
|
||||||
title?: InputMaybe<Scalars['String']>
|
title?: InputMaybe<Scalars['String']>
|
||||||
topic_slugs?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
topics?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
||||||
versionOf?: InputMaybe<Scalars['String']>
|
versionOf?: InputMaybe<Scalars['String']>
|
||||||
visibleForRoles?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
visibleForRoles?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
||||||
visibleForUsers?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
visibleForUsers?: InputMaybe<Array<InputMaybe<Scalars['String']>>>
|
||||||
|
|
|
@ -162,6 +162,36 @@ type InitialState = {
|
||||||
shouts?: Shout[]
|
shouts?: Shout[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TOP_MONTH_ARTICLES_COUNT = 10
|
||||||
|
|
||||||
|
export const loadTopMonthArticles = async (): Promise<void> => {
|
||||||
|
const articles = await apiClient.getShouts({
|
||||||
|
filters: {
|
||||||
|
visibility: 'public',
|
||||||
|
// TODO: replace with from, to
|
||||||
|
days: 30
|
||||||
|
},
|
||||||
|
order_by: 'rating_stat',
|
||||||
|
limit: TOP_MONTH_ARTICLES_COUNT
|
||||||
|
})
|
||||||
|
addArticles(articles)
|
||||||
|
setTopMonthArticles(articles)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TOP_ARTICLES_COUNT = 10
|
||||||
|
|
||||||
|
export const loadTopArticles = async (): Promise<void> => {
|
||||||
|
const articles = await apiClient.getShouts({
|
||||||
|
filters: {
|
||||||
|
visibility: 'public'
|
||||||
|
},
|
||||||
|
order_by: 'rating_stat',
|
||||||
|
limit: TOP_ARTICLES_COUNT
|
||||||
|
})
|
||||||
|
addArticles(articles)
|
||||||
|
setTopArticles(articles)
|
||||||
|
}
|
||||||
|
|
||||||
export const useArticlesStore = (initialState: InitialState = {}) => {
|
export const useArticlesStore = (initialState: InitialState = {}) => {
|
||||||
addArticles([...(initialState.shouts || [])])
|
addArticles([...(initialState.shouts || [])])
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,7 @@ import { apiClient } from '../../utils/apiClient'
|
||||||
|
|
||||||
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
export const follow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||||
await apiClient.follow({ what, slug })
|
await apiClient.follow({ what, slug })
|
||||||
// refresh session
|
|
||||||
// TODO: _store update code
|
|
||||||
}
|
}
|
||||||
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
export const unfollow = async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||||
await apiClient.unfollow({ what, slug })
|
await apiClient.unfollow({ what, slug })
|
||||||
// TODO: store update
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,11 +168,11 @@ export const apiClient = {
|
||||||
// subscribe
|
// subscribe
|
||||||
|
|
||||||
follow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
follow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||||
const response = await privateGraphQLClient.query(followMutation, { what, slug }).toPromise()
|
const response = await privateGraphQLClient.mutation(followMutation, { what, slug }).toPromise()
|
||||||
return response.data.follow
|
return response.data.follow
|
||||||
},
|
},
|
||||||
unfollow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
unfollow: async ({ what, slug }: { what: FollowingEntity; slug: string }) => {
|
||||||
const response = await privateGraphQLClient.query(unfollowMutation, { what, slug }).toPromise()
|
const response = await privateGraphQLClient.mutation(unfollowMutation, { what, slug }).toPromise()
|
||||||
return response.data.unfollow
|
return response.data.unfollow
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ export const apiClient = {
|
||||||
},
|
},
|
||||||
getAuthor: async ({ slug }: { slug: string }): Promise<Author> => {
|
getAuthor: async ({ slug }: { slug: string }): Promise<Author> => {
|
||||||
const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(authorBySlug, { slug }).toPromise()
|
||||||
console.debug('getAuthor', response)
|
// console.debug('getAuthor', response)
|
||||||
return response.data.getAuthor
|
return response.data.getAuthor
|
||||||
},
|
},
|
||||||
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
|
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
|
||||||
|
|
|
@ -65,13 +65,16 @@ const ru2en: { [key: string]: string } = {
|
||||||
я: 'ya'
|
я: 'ya'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const translit = (x: string, l?: string) => {
|
export const translit = (str: string) => {
|
||||||
let n
|
if (!str) {
|
||||||
|
return ''
|
||||||
n = [...(x || '')]
|
}
|
||||||
const isCyrillic = /[ЁА-яё]/.test(x || '')
|
|
||||||
|
const isCyrillic = /[ЁА-яё]/.test(str)
|
||||||
if (l !== 'ru' && isCyrillic) n = n.map((c: string) => ru2en[c] || c)
|
|
||||||
|
if (!isCyrillic) {
|
||||||
return n.join('')
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...str].map((c) => ru2en[c] || c).join('')
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,12 @@ export const restoreScrollPosition = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const scrollHandler = (elemId) => {
|
export const scrollHandler = (elemId: string, offset = -100) => {
|
||||||
const anchor = document.querySelector('#' + elemId)
|
const anchor = document.querySelector('#' + elemId)
|
||||||
// console.debug(elemId)
|
|
||||||
if (anchor) {
|
if (anchor) {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: anchor.getBoundingClientRect().top - 100,
|
top: anchor.getBoundingClientRect().top + offset,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user