263 lines
9.8 KiB
TypeScript
263 lines
9.8 KiB
TypeScript
import type { Author } from '../../graphql/schema/core.gen'
|
||
|
||
import { Meta } from '@solidjs/meta'
|
||
import { clsx } from 'clsx'
|
||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||
|
||
import { useFollowing } from '../../context/following'
|
||
import { useLocalize } from '../../context/localize'
|
||
import { useRouter } from '../../stores/router'
|
||
import { loadAuthors, setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
|
||
import { capitalize } from '../../utils/capitalize'
|
||
import { isCyrillic } from '../../utils/cyrillic'
|
||
import { dummyFilter } from '../../utils/dummyFilter'
|
||
import { getImageUrl } from '../../utils/getImageUrl'
|
||
import { translit } from '../../utils/ru2en'
|
||
import { scrollHandler } from '../../utils/scroll'
|
||
import { Loading } from '../_shared/Loading'
|
||
import { SearchField } from '../_shared/SearchField'
|
||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||
|
||
import styles from './AllAuthors.module.scss'
|
||
|
||
type AllAuthorsPageSearchParams = {
|
||
by: '' | 'name' | 'shouts' | 'followers'
|
||
}
|
||
|
||
type Props = {
|
||
authors: Author[]
|
||
isLoaded: boolean
|
||
}
|
||
|
||
const PAGE_SIZE = 20
|
||
|
||
export const AllAuthorsView = (props: Props) => {
|
||
const { t, lang } = useLocalize()
|
||
const ALPHABET =
|
||
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@']
|
||
const [offsetByShouts, setOffsetByShouts] = createSignal(0)
|
||
const [offsetByFollowers, setOffsetByFollowers] = createSignal(0)
|
||
const { searchParams, changeSearchParams } = useRouter<AllAuthorsPageSearchParams>()
|
||
const { sortedAuthors } = useAuthorsStore({
|
||
authors: props.authors,
|
||
sortBy: searchParams().by || 'name',
|
||
})
|
||
|
||
const [searchQuery, setSearchQuery] = createSignal('')
|
||
const offset = searchParams()?.by === 'shouts' ? offsetByShouts : offsetByFollowers
|
||
createEffect(() => {
|
||
let by = searchParams().by
|
||
if (by) {
|
||
setAuthorsSort(by)
|
||
} else {
|
||
by = 'name'
|
||
changeSearchParams({ by })
|
||
}
|
||
})
|
||
|
||
const loadMoreByShouts = async () => {
|
||
await loadAuthors({ by: { order: 'shouts_stat' }, limit: PAGE_SIZE, offset: offsetByShouts() })
|
||
setOffsetByShouts((o) => o + PAGE_SIZE)
|
||
}
|
||
const loadMoreByFollowers = async () => {
|
||
await loadAuthors({ by: { order: 'followers_stat' }, limit: PAGE_SIZE, offset: offsetByFollowers() })
|
||
setOffsetByFollowers((o) => o + PAGE_SIZE)
|
||
}
|
||
|
||
const isStatsLoaded = createMemo(() => sortedAuthors() && sortedAuthors().some((author) => author.stat))
|
||
|
||
createEffect(async () => {
|
||
if (!isStatsLoaded()) {
|
||
await loadMoreByShouts()
|
||
await loadMoreByFollowers()
|
||
}
|
||
})
|
||
|
||
const showMore = async () =>
|
||
await {
|
||
shouts: loadMoreByShouts,
|
||
followers: loadMoreByFollowers,
|
||
}[searchParams().by]()
|
||
const translate = (author: Author) =>
|
||
lang() === 'en' && isCyrillic(author.name)
|
||
? capitalize(translit(author.name.replace(/ё/, 'e').replace(/ь/, '')).replace(/-/, ' '), true)
|
||
: author.name
|
||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||
return sortedAuthors().reduce(
|
||
(acc, author) => {
|
||
let letter = ''
|
||
if (!letter && author && author.name) {
|
||
const name = translate(author)
|
||
.replace(/[^\dA-zА-я]/, ' ')
|
||
.trim()
|
||
const nameParts = name.trim().split(' ')
|
||
const found = nameParts.filter(Boolean).pop()
|
||
if (found && found.length > 0) {
|
||
letter = found[0].toUpperCase()
|
||
}
|
||
}
|
||
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@'
|
||
if (/[^A-z]/.test(letter) && lang() === 'en') letter = '@'
|
||
|
||
if (!acc[letter]) acc[letter] = []
|
||
author.name = translate(author)
|
||
acc[letter].push(author)
|
||
|
||
// Sort authors within each letter group alphabetically by name
|
||
acc[letter].sort((a, b) => a.name.localeCompare(b.name))
|
||
|
||
return acc
|
||
},
|
||
{} as { [letter: string]: Author[] },
|
||
)
|
||
})
|
||
|
||
const { isOwnerSubscribed } = useFollowing()
|
||
|
||
const sortedKeys = createMemo<string[]>(() => {
|
||
const keys = Object.keys(byLetter())
|
||
keys.sort()
|
||
keys.push(keys.shift())
|
||
return keys
|
||
})
|
||
|
||
const filteredAuthors = createMemo(() => {
|
||
return dummyFilter(sortedAuthors(), searchQuery(), lang())
|
||
})
|
||
|
||
const ogImage = getImageUrl('production/image/logo_image.png')
|
||
const ogTitle = t('Authors')
|
||
const description = t('List of authors of the open editorial community')
|
||
|
||
return (
|
||
<div class={clsx(styles.allAuthorsPage, 'wide-container')}>
|
||
<Meta name="descprition" content={description} />
|
||
<Meta name="keywords" content={t('keywords')} />
|
||
<Meta name="og:type" content="article" />
|
||
<Meta name="og:title" content={ogTitle} />
|
||
<Meta name="og:image" content={ogImage} />
|
||
<Meta name="twitter:image" content={ogImage} />
|
||
<Meta name="og:description" content={description} />
|
||
<Meta name="twitter:card" content="summary_large_image" />
|
||
<Meta name="twitter:title" content={ogTitle} />
|
||
<Meta name="twitter:description" content={description} />
|
||
<Show when={props.isLoaded} fallback={<Loading />}>
|
||
<div class="offset-md-5">
|
||
<div class="row">
|
||
<div class="col-lg-20 col-xl-18">
|
||
<h1>{t('Authors')}</h1>
|
||
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
||
<Show when={isStatsLoaded()}>
|
||
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
||
<li
|
||
classList={{
|
||
'view-switcher__item--selected': !searchParams().by || searchParams().by === 'shouts',
|
||
}}
|
||
>
|
||
<a href="/authors?by=shouts">{t('By shouts')}</a>
|
||
</li>
|
||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'followers' }}>
|
||
<a href="/authors?by=followers">{t('By popularity')}</a>
|
||
</li>
|
||
<li classList={{ 'view-switcher__item--selected': searchParams().by === 'name' }}>
|
||
<a href="/authors?by=name">{t('By name')}</a>
|
||
</li>
|
||
<Show when={searchParams().by !== 'name'}>
|
||
<li class="view-switcher__search">
|
||
<SearchField onChange={(value) => setSearchQuery(value)} />
|
||
</li>
|
||
</Show>
|
||
</ul>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
|
||
<Show when={sortedAuthors().length > 0}>
|
||
<Show when={searchParams().by === 'name'}>
|
||
<div class="row">
|
||
<div class="col-lg-20 col-xl-18">
|
||
<ul class={clsx('nodash', styles.alphabet)}>
|
||
<For each={ALPHABET}>
|
||
{(letter, index) => (
|
||
<li>
|
||
<Show when={letter in byLetter()} fallback={letter}>
|
||
<a
|
||
href={`/authors?by=name#letter-${index()}`}
|
||
onClick={(event) => {
|
||
event.preventDefault()
|
||
scrollHandler(`letter-${index()}`)
|
||
}}
|
||
>
|
||
{letter}
|
||
</a>
|
||
</Show>
|
||
</li>
|
||
)}
|
||
</For>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<For each={sortedKeys()}>
|
||
{(letter) => (
|
||
<div class={clsx(styles.group, 'group')}>
|
||
<h2 id={`letter-${ALPHABET.indexOf(letter)}`}>{letter}</h2>
|
||
<div class="container">
|
||
<div class="row">
|
||
<div class="col-lg-20">
|
||
<div class="row">
|
||
<For each={byLetter()[letter]}>
|
||
{(author) => (
|
||
<div class={clsx(styles.topic, 'topic col-sm-12 col-md-8')}>
|
||
<div class="topic-title">
|
||
<a href={`/author/${author.slug}`}>{translate(author)}</a>
|
||
<Show when={author.stat}>
|
||
<span class={styles.articlesCounter}>{author.stat.shouts}</span>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</Show>
|
||
|
||
<Show when={searchParams().by && searchParams().by !== 'name'}>
|
||
<For each={filteredAuthors().slice(0, PAGE_SIZE)}>
|
||
{(author) => (
|
||
<div class="row">
|
||
<div class="col-lg-20 col-xl-18">
|
||
<AuthorBadge
|
||
author={author as Author}
|
||
isFollowed={{
|
||
loaded: Boolean(filteredAuthors()),
|
||
value: isOwnerSubscribed(author.id),
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</Show>
|
||
|
||
<Show when={filteredAuthors().length > PAGE_SIZE + offset() && searchParams().by !== 'name'}>
|
||
<div class="row">
|
||
<div class={clsx(styles.loadMoreContainer, 'col-24 col-md-20')}>
|
||
<button class={clsx('button', styles.loadMoreButton)} onClick={showMore}>
|
||
{t('Load more')}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
)
|
||
}
|