Feature/all authors order (#410)
Load Authors by btn click --------- Co-authored-by: Untone <anton.rewin@gmail.com>
This commit is contained in:
parent
01a4b558bd
commit
4e931a39c5
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npm run pre-commit
|
|
@ -291,6 +291,7 @@
|
|||
"Profile": "Profile",
|
||||
"Publications": "Publications",
|
||||
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} publications}}",
|
||||
"FollowersWithCount": "{count, plural, =0 {no followers} one {{count} follower} other {{count} followers}}",
|
||||
"Publish Album": "Publish Album",
|
||||
"Publish Settings": "Publish Settings",
|
||||
"Published": "Published",
|
||||
|
|
|
@ -309,9 +309,10 @@
|
|||
"Publication settings": "Настройки публикации",
|
||||
"Publications": "Публикации",
|
||||
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}",
|
||||
"FollowersWithCount": "{count, plural, =0 {нет подписчиков} one {{count} подписчик} few {{count} подписчика} other {{count} подписчиков}}",
|
||||
"Publish": "Опубликовать",
|
||||
"Publish Album": "Опубликовать альбом",
|
||||
"Publish Settings": "Настройки публикации",
|
||||
"Publish": "Опубликовать",
|
||||
"Published": "Опубликованные",
|
||||
"Punchline": "Панчлайн",
|
||||
"Quit": "Выйти",
|
||||
|
|
|
@ -58,6 +58,11 @@
|
|||
}
|
||||
|
||||
.bio {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
color: var(--black-400);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
|
@ -118,12 +118,17 @@ export const AuthorBadge = (props: Props) => {
|
|||
<Match when={props.author.bio}>
|
||||
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} />
|
||||
</Match>
|
||||
<Match when={props.author?.stat && props.author?.stat.shouts > 0}>
|
||||
<div class={styles.bio}>
|
||||
{t('PublicationsWithCount', { count: props.author?.stat.shouts ?? 0 })}
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={props.author?.stat}>
|
||||
<div class={styles.bio}>
|
||||
<Show when={props.author?.stat.shouts > 0}>
|
||||
<div>{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}</div>
|
||||
</Show>
|
||||
<Show when={props.author?.stat.followers > 0}>
|
||||
<div>{t('FollowersWithCount', { count: props.author.stat?.followers ?? 0 })}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</ConditionalWrapper>
|
||||
</div>
|
||||
|
|
26
src/components/AuthorsList/AuthorsList.module.scss
Normal file
26
src/components/AuthorsList/AuthorsList.module.scss
Normal file
|
@ -0,0 +1,26 @@
|
|||
.AuthorsList {
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 8rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
opacity: 0.5;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
90
src/components/AuthorsList/AuthorsList.tsx
Normal file
90
src/components/AuthorsList/AuthorsList.tsx
Normal file
|
@ -0,0 +1,90 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createSignal } from 'solid-js'
|
||||
import { useFollowing } from '../../context/following'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { apiClient } from '../../graphql/client/core'
|
||||
import { setAuthorsByFollowers, setAuthorsByShouts, useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { InlineLoader } from '../InlineLoader'
|
||||
import { Button } from '../_shared/Button'
|
||||
import styles from './AuthorsList.module.scss'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
query: 'shouts' | 'followers'
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
export const AuthorsList = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
const { isOwnerSubscribed } = useFollowing()
|
||||
const [loading, setLoading] = createSignal(false)
|
||||
const [currentPage, setCurrentPage] = createSignal({ shouts: 0, followers: 0 })
|
||||
const { authorsByShouts, authorsByFollowers } = useAuthorsStore()
|
||||
|
||||
const fetchAuthors = async (queryType: 'shouts' | 'followers', page: number) => {
|
||||
setLoading(true)
|
||||
const offset = PAGE_SIZE * page
|
||||
const result = await apiClient.loadAuthorsBy({
|
||||
by: { order: queryType },
|
||||
limit: PAGE_SIZE,
|
||||
offset: offset,
|
||||
})
|
||||
|
||||
if (queryType === 'shouts') {
|
||||
setAuthorsByShouts((prev) => [...prev, ...result])
|
||||
} else {
|
||||
setAuthorsByFollowers((prev) => [...prev, ...result])
|
||||
}
|
||||
setLoading(false)
|
||||
return result
|
||||
}
|
||||
|
||||
const loadMoreAuthors = () => {
|
||||
const queryType = props.query
|
||||
const nextPage = currentPage()[queryType] + 1
|
||||
fetchAuthors(queryType, nextPage).then(() =>
|
||||
setCurrentPage({ ...currentPage(), [queryType]: nextPage }),
|
||||
)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const queryType = props.query
|
||||
if (
|
||||
currentPage()[queryType] === 0 &&
|
||||
(authorsByShouts().length === 0 || authorsByFollowers().length === 0)
|
||||
) {
|
||||
loadMoreAuthors()
|
||||
}
|
||||
})
|
||||
|
||||
const authorsList = () => (props.query === 'shouts' ? authorsByShouts() : authorsByFollowers())
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.AuthorsList, props.class)}>
|
||||
<For each={authorsList()}>
|
||||
{(author) => (
|
||||
<div class="row">
|
||||
<div class="col-lg-20 col-xl-18">
|
||||
<AuthorBadge
|
||||
author={author}
|
||||
isFollowed={{
|
||||
loaded: !loading(),
|
||||
value: isOwnerSubscribed(author.id),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class={styles.action}>
|
||||
<Show when={!loading()}>
|
||||
<Button value={t('Load more')} onClick={loadMoreAuthors} />
|
||||
</Show>
|
||||
<Show when={loading()}>
|
||||
<InlineLoader />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/AuthorsList/index.ts
Normal file
1
src/components/AuthorsList/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AuthorsList } from './AuthorsList'
|
18
src/components/InlineLoader/InlineLoader.module.scss
Normal file
18
src/components/InlineLoader/InlineLoader.module.scss
Normal file
|
@ -0,0 +1,18 @@
|
|||
.InlineLoader {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
opacity: 0.5;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
}
|
20
src/components/InlineLoader/InlineLoader.tsx
Normal file
20
src/components/InlineLoader/InlineLoader.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { clsx } from 'clsx'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Loading } from '../_shared/Loading'
|
||||
import styles from './InlineLoader.module.scss'
|
||||
|
||||
type Props = {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export const InlineLoader = (props: Props) => {
|
||||
const { t } = useLocalize()
|
||||
return (
|
||||
<div class={styles.InlineLoader}>
|
||||
<div class={styles.icon}>
|
||||
<Loading size="tiny" />
|
||||
</div>
|
||||
<div>{t('Loading')}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/InlineLoader/index.ts
Normal file
1
src/components/InlineLoader/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { InlineLoader } from './InlineLoader'
|
|
@ -1,234 +0,0 @@
|
|||
import type { Author } from '../../graphql/schema/core.gen'
|
||||
|
||||
import { Meta } from '@solidjs/meta'
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createMemo, createSignal } 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 { dummyFilter } from '../../utils/dummyFilter'
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
import { scrollHandler } from '../../utils/scroll'
|
||||
import { authorLetterReduce, translateAuthor } from '../../utils/translate'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { Loading } from '../_shared/Loading'
|
||||
import { SearchField } from '../_shared/SearchField'
|
||||
|
||||
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()?.some((author) => author.stat))
|
||||
|
||||
createEffect(async () => {
|
||||
if (!isStatsLoaded()) {
|
||||
await loadMoreByShouts()
|
||||
await loadMoreByFollowers()
|
||||
}
|
||||
})
|
||||
|
||||
const showMore = async () =>
|
||||
await {
|
||||
shouts: loadMoreByShouts,
|
||||
followers: loadMoreByFollowers,
|
||||
}[searchParams().by]()
|
||||
|
||||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||
return sortedAuthors().reduce(
|
||||
(acc, author) => authorLetterReduce(acc, author, lang()),
|
||||
{} 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}`}>{translateAuthor(author, lang())}</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>
|
||||
)
|
||||
}
|
|
@ -81,3 +81,5 @@
|
|||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
177
src/components/Views/AllAuthors/AllAuthors.tsx
Normal file
177
src/components/Views/AllAuthors/AllAuthors.tsx
Normal file
|
@ -0,0 +1,177 @@
|
|||
import type { Author } from '../../../graphql/schema/core.gen'
|
||||
|
||||
import { Meta } from '@solidjs/meta'
|
||||
import { clsx } from 'clsx'
|
||||
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'
|
||||
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useRouter } from '../../../stores/router'
|
||||
import { setAuthorsSort, useAuthorsStore } from '../../../stores/zine/authors'
|
||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||
import { scrollHandler } from '../../../utils/scroll'
|
||||
import { authorLetterReduce, translateAuthor } from '../../../utils/translate'
|
||||
|
||||
import { AuthorsList } from '../../AuthorsList'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { SearchField } from '../../_shared/SearchField'
|
||||
|
||||
import styles from './AllAuthors.module.scss'
|
||||
|
||||
type AllAuthorsPageSearchParams = {
|
||||
by: '' | 'name' | 'shouts' | 'followers'
|
||||
}
|
||||
|
||||
type Props = {
|
||||
authors: Author[]
|
||||
isLoaded: boolean
|
||||
}
|
||||
|
||||
export const AllAuthors = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
const ALPHABET =
|
||||
lang() === 'ru' ? [...'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ@'] : [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ@']
|
||||
const { searchParams, changeSearchParams } = useRouter<AllAuthorsPageSearchParams>()
|
||||
const { sortedAuthors } = useAuthorsStore({
|
||||
authors: props.authors,
|
||||
sortBy: searchParams().by || 'name',
|
||||
})
|
||||
|
||||
const [searchQuery, setSearchQuery] = createSignal('')
|
||||
|
||||
createEffect(() => {
|
||||
let by = searchParams().by
|
||||
if (by) {
|
||||
setAuthorsSort(by)
|
||||
} else {
|
||||
by = 'name'
|
||||
changeSearchParams({ by })
|
||||
}
|
||||
})
|
||||
|
||||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||
return sortedAuthors().reduce(
|
||||
(acc, author) => authorLetterReduce(acc, author, lang()),
|
||||
{} as { [letter: string]: Author[] },
|
||||
)
|
||||
})
|
||||
|
||||
const sortedKeys = createMemo<string[]>(() => {
|
||||
const keys = Object.keys(byLetter())
|
||||
keys.sort()
|
||||
keys.push(keys.shift())
|
||||
return keys
|
||||
})
|
||||
|
||||
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>
|
||||
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
||||
<li
|
||||
class={clsx({
|
||||
['view-switcher__item--selected']: !searchParams().by || searchParams().by === 'shouts',
|
||||
})}
|
||||
>
|
||||
<a href="/authors?by=shouts">{t('By shouts')}</a>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
['view-switcher__item--selected']: searchParams().by === 'followers',
|
||||
})}
|
||||
>
|
||||
<a href="/authors?by=followers">{t('By popularity')}</a>
|
||||
</li>
|
||||
<li
|
||||
class={clsx({
|
||||
['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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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}`}>{translateAuthor(author, lang())}</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 !== 'name' && props.isLoaded} fallback={<Loading />}>
|
||||
<AuthorsList query={searchParams().by === 'shouts' ? 'shouts' : 'followers'} />
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
1
src/components/Views/AllAuthors/index.ts
Normal file
1
src/components/Views/AllAuthors/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AllAuthors } from './AllAuthors'
|
|
@ -50,24 +50,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
opacity: 0.5;
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.teaser {
|
||||
min-height: 300px;
|
||||
display: flex;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Button } from '../Button'
|
|||
import { DropdownSelect } from '../DropdownSelect'
|
||||
import { Loading } from '../Loading'
|
||||
|
||||
import { InlineLoader } from '../../InlineLoader'
|
||||
import styles from './InviteMembers.module.scss'
|
||||
|
||||
type InviteAuthor = Author & { selected: boolean }
|
||||
|
@ -62,7 +63,7 @@ export const InviteMembers = (props: Props) => {
|
|||
return authors?.slice(start, end)
|
||||
}
|
||||
|
||||
const [pages, _infiniteScrollLoader, { end }] = createInfiniteScroll(fetcher)
|
||||
const [pages, setEl, { end }] = createInfiniteScroll(fetcher)
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
|
@ -158,11 +159,8 @@ export const InviteMembers = (props: Props) => {
|
|||
)}
|
||||
</For>
|
||||
<Show when={!end()}>
|
||||
<div use:infiniteScrollLoader class={styles.loading}>
|
||||
<div class={styles.icon}>
|
||||
<Loading size="tiny" />
|
||||
</div>
|
||||
<div>{t('Loading')}</div>
|
||||
<div ref={setEl as (e: HTMLDivElement) => void}>
|
||||
<InlineLoader />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { PageProps } from './types'
|
||||
|
||||
import { createSignal, onMount } from 'solid-js'
|
||||
import { createEffect, createSignal, onMount } from 'solid-js'
|
||||
|
||||
import { AllAuthorsView } from '../components/Views/AllAuthors'
|
||||
import { AllAuthors } from '../components/Views/AllAuthors/'
|
||||
import { PageLayout } from '../components/_shared/PageLayout'
|
||||
import { useLocalize } from '../context/localize'
|
||||
import { loadAllAuthors } from '../stores/zine/authors'
|
||||
|
@ -23,7 +23,7 @@ export const AllAuthorsPage = (props: PageProps) => {
|
|||
|
||||
return (
|
||||
<PageLayout title={t('Authors')}>
|
||||
<AllAuthorsView isLoaded={isLoaded()} authors={props.allAuthors} />
|
||||
<AllAuthors isLoaded={isLoaded()} authors={props.allAuthors} />
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
|||
import { byStat } from '../../utils/sortby'
|
||||
|
||||
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
||||
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
||||
|
||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
||||
|
||||
|
@ -13,6 +14,11 @@ export const setAuthorsSort = (sortBy: AuthorsSortBy) => setSortAllBy(sortBy)
|
|||
|
||||
const [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({})
|
||||
const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: string]: Author[] }>({})
|
||||
const [authorsByShouts, setSortedAuthorsByShout] = createSignal<Author[]>([])
|
||||
const [authorsByFollowers, setSortedAuthorsByFollowers] = createSignal<Author[]>([])
|
||||
|
||||
export const setAuthorsByShouts = (authors: SortedAuthorsSetter) => setSortedAuthorsByShout(authors)
|
||||
export const setAuthorsByFollowers = (authors: SortedAuthorsSetter) => setSortedAuthorsByFollowers(authors)
|
||||
|
||||
const sortedAuthors = createLazyMemo(() => {
|
||||
const authors = Object.values(authorEntities())
|
||||
|
@ -108,5 +114,5 @@ export const useAuthorsStore = (initialState: InitialState = {}) => {
|
|||
}
|
||||
addAuthors([...(initialState.authors || [])])
|
||||
|
||||
return { authorEntities, sortedAuthors, authorsByTopic }
|
||||
return { authorEntities, sortedAuthors, authorsByTopic, authorsByShouts, authorsByFollowers }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user