home page tops fix, all authors, all topics fixes and more

This commit is contained in:
Igor Lobanov 2022-12-01 19:45:35 +01:00
parent c086a242b7
commit e61181392c
35 changed files with 369 additions and 282 deletions

View File

@ -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'

View File

@ -20,5 +20,3 @@ export const RatingControl = (props: RatingControlProps) => {
</div> </div>
) )
} }
export default RatingControl

View File

@ -277,3 +277,7 @@
margin-top: -0.6em; margin-top: -0.6em;
} }
} }
.isSubscribing {
color: transparent;
}

View File

@ -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>
) )

View File

@ -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 })

View File

@ -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 (
<> <>

View File

@ -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}

View File

@ -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) {

View File

@ -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>

View File

@ -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)}>

View File

@ -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;

View File

@ -116,3 +116,7 @@
.buttonCompact { .buttonCompact {
margin-top: 0.6rem; margin-top: 0.6rem;
} }
.isSubscribing {
color: transparent;
}

View File

@ -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>
) )

View File

@ -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}>

View File

@ -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')}

View File

@ -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' },

View File

@ -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) => (

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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 }
}, },

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -4,6 +4,7 @@ export default gql`
query GetCollabsQuery { query GetCollabsQuery {
getCollabs { getCollabs {
authors { authors {
id
slug slug
name name
pic pic

View File

@ -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

View File

@ -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']>>>

View File

@ -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 || [])])

View File

@ -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
} }

View File

@ -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> => {

View File

@ -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('')
} }

View File

@ -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'
}) })
} }