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