Merge branch 'dev' of github.com:Discours/discoursio-webapp into dev
This commit is contained in:
commit
5ca261710c
|
@ -1,4 +0,0 @@
|
||||||
#!/usr/bin/env sh
|
|
||||||
. "$(dirname -- "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npm run pre-commit
|
|
|
@ -291,6 +291,7 @@
|
||||||
"Profile": "Profile",
|
"Profile": "Profile",
|
||||||
"Publications": "Publications",
|
"Publications": "Publications",
|
||||||
"PublicationsWithCount": "{count, plural, =0 {no publications} one {{count} publication} other {{count} 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 Album": "Publish Album",
|
||||||
"Publish Settings": "Publish Settings",
|
"Publish Settings": "Publish Settings",
|
||||||
"Published": "Published",
|
"Published": "Published",
|
||||||
|
|
|
@ -309,9 +309,10 @@
|
||||||
"Publication settings": "Настройки публикации",
|
"Publication settings": "Настройки публикации",
|
||||||
"Publications": "Публикации",
|
"Publications": "Публикации",
|
||||||
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}",
|
"PublicationsWithCount": "{count, plural, =0 {нет публикаций} one {{count} публикация} few {{count} публикации} other {{count} публикаций}}",
|
||||||
|
"FollowersWithCount": "{count, plural, =0 {нет подписчиков} one {{count} подписчик} few {{count} подписчика} other {{count} подписчиков}}",
|
||||||
|
"Publish": "Опубликовать",
|
||||||
"Publish Album": "Опубликовать альбом",
|
"Publish Album": "Опубликовать альбом",
|
||||||
"Publish Settings": "Настройки публикации",
|
"Publish Settings": "Настройки публикации",
|
||||||
"Publish": "Опубликовать",
|
|
||||||
"Published": "Опубликованные",
|
"Published": "Опубликованные",
|
||||||
"Punchline": "Панчлайн",
|
"Punchline": "Панчлайн",
|
||||||
"Quit": "Выйти",
|
"Quit": "Выйти",
|
||||||
|
|
|
@ -38,12 +38,17 @@ export const Comment = (props: Props) => {
|
||||||
const [loading, setLoading] = createSignal(false)
|
const [loading, setLoading] = createSignal(false)
|
||||||
const [editMode, setEditMode] = createSignal(false)
|
const [editMode, setEditMode] = createSignal(false)
|
||||||
const [clearEditor, setClearEditor] = createSignal(false)
|
const [clearEditor, setClearEditor] = createSignal(false)
|
||||||
const { author } = useSession()
|
const { author, session } = useSession()
|
||||||
const { createReaction, deleteReaction, updateReaction } = useReactions()
|
const { createReaction, deleteReaction, updateReaction } = useReactions()
|
||||||
const { showConfirm } = useConfirm()
|
const { showConfirm } = useConfirm()
|
||||||
const { showSnackbar } = useSnackbar()
|
const { showSnackbar } = useSnackbar()
|
||||||
|
|
||||||
const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === author()?.slug)
|
const canEdit = createMemo(
|
||||||
|
() =>
|
||||||
|
Boolean(author()?.id) &&
|
||||||
|
(props.comment?.created_by?.id === author().id || session()?.user?.roles.includes('editor')),
|
||||||
|
)
|
||||||
|
|
||||||
const comment = createMemo(() => props.comment)
|
const comment = createMemo(() => props.comment)
|
||||||
const body = createMemo(() => (comment().body || '').trim())
|
const body = createMemo(() => (comment().body || '').trim())
|
||||||
|
|
||||||
|
@ -93,7 +98,8 @@ export const Comment = (props: Props) => {
|
||||||
const handleUpdate = async (value) => {
|
const handleUpdate = async (value) => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await updateReaction(props.comment.id, {
|
await updateReaction({
|
||||||
|
id: props.comment.id,
|
||||||
kind: ReactionKind.Comment,
|
kind: ReactionKind.Comment,
|
||||||
body: value,
|
body: value,
|
||||||
shout: props.comment.shout.id,
|
shout: props.comment.shout.id,
|
||||||
|
@ -108,9 +114,7 @@ export const Comment = (props: Props) => {
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
id={`comment_${comment().id}`}
|
id={`comment_${comment().id}`}
|
||||||
class={clsx(styles.comment, props.class, {
|
class={clsx(styles.comment, props.class, { [styles.isNew]: comment()?.created_at > props.lastSeen })}
|
||||||
[styles.isNew]: !isCommentAuthor() && comment()?.created_at > props.lastSeen,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Show when={!!body()}>
|
<Show when={!!body()}>
|
||||||
<div class={styles.commentContent}>
|
<div class={styles.commentContent}>
|
||||||
|
@ -189,7 +193,7 @@ export const Comment = (props: Props) => {
|
||||||
{loading() ? t('Loading') : t('Reply')}
|
{loading() ? t('Loading') : t('Reply')}
|
||||||
</button>
|
</button>
|
||||||
</ShowIfAuthenticated>
|
</ShowIfAuthenticated>
|
||||||
<Show when={isCommentAuthor()}>
|
<Show when={canEdit()}>
|
||||||
<button
|
<button
|
||||||
class={clsx(styles.commentControl, styles.commentControlEdit)}
|
class={clsx(styles.commentControl, styles.commentControlEdit)}
|
||||||
onClick={toggleEditMode}
|
onClick={toggleEditMode}
|
||||||
|
|
|
@ -66,9 +66,6 @@ export const CommentsTree = (props: Props) => {
|
||||||
if (commentsOrder() === 'rating') {
|
if (commentsOrder() === 'rating') {
|
||||||
newSortedComments = newSortedComments.sort(sortCommentsByRating)
|
newSortedComments = newSortedComments.sort(sortCommentsByRating)
|
||||||
}
|
}
|
||||||
|
|
||||||
newSortedComments.reverse()
|
|
||||||
|
|
||||||
return newSortedComments
|
return newSortedComments
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -58,9 +58,8 @@ export type ArticlePageSearchParams = {
|
||||||
|
|
||||||
const scrollTo = (el: HTMLElement) => {
|
const scrollTo = (el: HTMLElement) => {
|
||||||
const { top } = el.getBoundingClientRect()
|
const { top } = el.getBoundingClientRect()
|
||||||
|
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
|
top: top - DEFAULT_HEADER_OFFSET,
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
})
|
})
|
||||||
|
@ -75,10 +74,17 @@ export const FullArticle = (props: Props) => {
|
||||||
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
|
||||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||||
const { t, formatDate, lang } = useLocalize()
|
const { t, formatDate, lang } = useLocalize()
|
||||||
const { author, isAuthenticated, requireAuthentication } = useSession()
|
const { author, session, isAuthenticated, requireAuthentication } = useSession()
|
||||||
|
|
||||||
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
|
||||||
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
|
|
||||||
|
const canEdit = createMemo(
|
||||||
|
() =>
|
||||||
|
Boolean(author()?.id) &&
|
||||||
|
(props.article?.authors?.some((a) => Boolean(a) && a?.id === author().id) ||
|
||||||
|
props.article?.created_by?.id === author().id ||
|
||||||
|
session()?.user?.roles.includes('editor')),
|
||||||
|
)
|
||||||
|
|
||||||
const mainTopic = createMemo(() => {
|
const mainTopic = createMemo(() => {
|
||||||
const mainTopicSlug = props.article.topics.length > 0 ? props.article.main_topic : null
|
const mainTopicSlug = props.article.topics.length > 0 ? props.article.main_topic : null
|
||||||
|
@ -145,22 +151,16 @@ export const FullArticle = (props: Props) => {
|
||||||
current: HTMLDivElement
|
current: HTMLDivElement
|
||||||
} = { current: null }
|
} = { current: null }
|
||||||
|
|
||||||
const scrollToComments = () => {
|
|
||||||
scrollTo(commentsRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.scrollToComments) {
|
if (props.scrollToComments) {
|
||||||
scrollToComments()
|
scrollTo(commentsRef.current)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
|
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
|
||||||
scrollToComments()
|
requestAnimationFrame(() => scrollTo(commentsRef.current))
|
||||||
changeSearchParams({
|
changeSearchParams({ scrollTo: null })
|
||||||
scrollTo: null,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -170,10 +170,8 @@ export const FullArticle = (props: Props) => {
|
||||||
`[id='comment_${searchParams().commentId}']`,
|
`[id='comment_${searchParams().commentId}']`,
|
||||||
)
|
)
|
||||||
|
|
||||||
changeSearchParams({ commentId: null })
|
|
||||||
|
|
||||||
if (commentElement) {
|
if (commentElement) {
|
||||||
scrollTo(commentElement)
|
requestAnimationFrame(() => scrollTo(commentElement))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -466,7 +464,11 @@ export const FullArticle = (props: Props) => {
|
||||||
|
|
||||||
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
|
<Popover content={t('Comment')} disabled={isActionPopupActive()}>
|
||||||
{(triggerRef: (el) => void) => (
|
{(triggerRef: (el) => void) => (
|
||||||
<div class={clsx(styles.shoutStatsItem)} ref={triggerRef} onClick={scrollToComments}>
|
<div
|
||||||
|
class={clsx(styles.shoutStatsItem)}
|
||||||
|
ref={triggerRef}
|
||||||
|
onClick={() => scrollTo(commentsRef.current)}
|
||||||
|
>
|
||||||
<Icon name="comment" class={styles.icon} />
|
<Icon name="comment" class={styles.icon} />
|
||||||
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
|
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||||
<Show
|
<Show
|
||||||
|
@ -544,7 +546,7 @@ export const FullArticle = (props: Props) => {
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<FeedArticlePopup
|
<FeedArticlePopup
|
||||||
isOwner={canEdit()}
|
canEdit={canEdit()}
|
||||||
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
|
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
|
||||||
onShareClick={() => showModal('share')}
|
onShareClick={() => showModal('share')}
|
||||||
onInviteClick={() => showModal('inviteMembers')}
|
onInviteClick={() => showModal('inviteMembers')}
|
||||||
|
|
|
@ -58,6 +58,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.bio {
|
.bio {
|
||||||
|
@include font-size(1.2rem);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
color: var(--black-400);
|
color: var(--black-400);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,12 +118,17 @@ export const AuthorBadge = (props: Props) => {
|
||||||
<Match when={props.author.bio}>
|
<Match when={props.author.bio}>
|
||||||
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} />
|
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} />
|
||||||
</Match>
|
</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>
|
</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>
|
</Show>
|
||||||
</ConditionalWrapper>
|
</ConditionalWrapper>
|
||||||
</div>
|
</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'
|
|
@ -106,7 +106,7 @@ const LAYOUT_ASPECT = {
|
||||||
|
|
||||||
export const ArticleCard = (props: ArticleCardProps) => {
|
export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
const { t, lang, formatDate } = useLocalize()
|
const { t, lang, formatDate } = useLocalize()
|
||||||
const { author } = useSession()
|
const { author, session } = useSession()
|
||||||
const { changeSearchParams } = useRouter()
|
const { changeSearchParams } = useRouter()
|
||||||
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
|
||||||
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
|
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
|
||||||
|
@ -120,9 +120,13 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '',
|
props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '',
|
||||||
)
|
)
|
||||||
|
|
||||||
const canEdit = () =>
|
const canEdit = createMemo(
|
||||||
props.article.authors?.some((a) => a && a?.slug === author()?.slug) ||
|
() =>
|
||||||
props.article.created_by?.id === author()?.id
|
Boolean(author()?.id) &&
|
||||||
|
(props.article?.authors?.some((a) => Boolean(a) && a?.id === author().id) ||
|
||||||
|
props.article?.created_by?.id === author().id ||
|
||||||
|
session()?.user?.roles.includes('editor')),
|
||||||
|
)
|
||||||
|
|
||||||
const scrollToComments = (event) => {
|
const scrollToComments = (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
@ -365,7 +369,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
||||||
|
|
||||||
<div class={styles.shoutCardDetailsItem}>
|
<div class={styles.shoutCardDetailsItem}>
|
||||||
<FeedArticlePopup
|
<FeedArticlePopup
|
||||||
isOwner={canEdit()}
|
canEdit={canEdit()}
|
||||||
containerCssClass={stylesHeader.control}
|
containerCssClass={stylesHeader.control}
|
||||||
onShareClick={() => props.onShare(props.article)}
|
onShareClick={() => props.onShare(props.article)}
|
||||||
onInviteClick={props.onInvite}
|
onInviteClick={props.onInvite}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { SoonChip } from '../../_shared/SoonChip'
|
||||||
import styles from './FeedArticlePopup.module.scss'
|
import styles from './FeedArticlePopup.module.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOwner: boolean
|
canEdit: boolean
|
||||||
onInviteClick: () => void
|
onInviteClick: () => void
|
||||||
onShareClick: () => void
|
onShareClick: () => void
|
||||||
} & Omit<PopupProps, 'children'>
|
} & Omit<PopupProps, 'children'>
|
||||||
|
@ -41,7 +41,7 @@ export const FeedArticlePopup = (props: Props) => {
|
||||||
{t('Share')}
|
{t('Share')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<Show when={!props.isOwner}>
|
<Show when={!props.canEdit}>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
class={styles.action}
|
class={styles.action}
|
||||||
|
@ -67,7 +67,7 @@ export const FeedArticlePopup = (props: Props) => {
|
||||||
{t('Invite experts')}
|
{t('Invite experts')}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<Show when={!props.isOwner}>
|
<Show when={!props.canEdit}>
|
||||||
<li>
|
<li>
|
||||||
<button class={clsx(styles.action, styles.soon)} role="button">
|
<button class={clsx(styles.action, styles.soon)} role="button">
|
||||||
{t('Subscribe to comments')} <SoonChip />
|
{t('Subscribe to comments')} <SoonChip />
|
||||||
|
@ -79,7 +79,7 @@ export const FeedArticlePopup = (props: Props) => {
|
||||||
{t('Add to bookmarks')} <SoonChip />
|
{t('Add to bookmarks')} <SoonChip />
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/*<Show when={!props.isOwner}>*/}
|
{/*<Show when={!props.canEdit}>*/}
|
||||||
{/* <li>*/}
|
{/* <li>*/}
|
||||||
{/* <button*/}
|
{/* <button*/}
|
||||||
{/* class={styles.action}*/}
|
{/* class={styles.action}*/}
|
||||||
|
|
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'
|
|
@ -17,11 +17,11 @@ type Props = {
|
||||||
|
|
||||||
export const Link = (props: Props) => {
|
export const Link = (props: Props) => {
|
||||||
const { page } = useRouter()
|
const { page } = useRouter()
|
||||||
const isSelected = page().route === props.routeName
|
const isSelected = page()?.route === props.routeName
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
onClick={props.onClick}
|
onClick={props.onClick}
|
||||||
classList={{ 'view-switcher__item--selected': page().route === props.routeName }}
|
classList={{ 'view-switcher__item--selected': page()?.route === props.routeName }}
|
||||||
>
|
>
|
||||||
<ConditionalWrapper
|
<ConditionalWrapper
|
||||||
condition={!isSelected && Boolean(props.routeName)}
|
condition={!isSelected && Boolean(props.routeName)}
|
||||||
|
|
|
@ -75,7 +75,7 @@ export const TopicBadge = (props: Props) => {
|
||||||
when={props.topic.body}
|
when={props.topic.body}
|
||||||
fallback={
|
fallback={
|
||||||
<div class={styles.description}>
|
<div class={styles.description}>
|
||||||
{t('PublicationsWithCount', { count: props.topic.stat.shouts ?? 0 })}
|
{t('PublicationsWithCount', { count: props.topic?.stat?.shouts ?? 0 })}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
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'
|
|
@ -71,13 +71,7 @@ export const AuthorView = (props: Props) => {
|
||||||
const fetchData = async (slug) => {
|
const fetchData = async (slug) => {
|
||||||
try {
|
try {
|
||||||
const [subscriptionsResult, followersResult] = await Promise.all([
|
const [subscriptionsResult, followersResult] = await Promise.all([
|
||||||
(async () => {
|
apiClient.getAuthorFollows({ slug }),
|
||||||
const [getAuthors, getTopics] = await Promise.all([
|
|
||||||
apiClient.getAuthorFollowingAuthors({ slug }),
|
|
||||||
apiClient.getAuthorFollowingTopics({ slug }),
|
|
||||||
])
|
|
||||||
return { authors: getAuthors, topics: getTopics }
|
|
||||||
})(),
|
|
||||||
apiClient.getAuthorFollowers({ slug }),
|
apiClient.getAuthorFollowers({ slug }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -69,10 +69,12 @@ export const HomeView = (props: Props) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await apiClient.getRandomTopicShouts(RANDOM_TOPIC_SHOUTS_COUNT)
|
const result = await apiClient.getRandomTopicShouts(RANDOM_TOPIC_SHOUTS_COUNT)
|
||||||
if (!result) console.warn('[apiClient.getRandomTopicShouts] failed')
|
if (!result || result.error) console.warn('[apiClient.getRandomTopicShouts] failed')
|
||||||
batch(() => {
|
batch(() => {
|
||||||
|
if (!result?.error) {
|
||||||
if (result?.topic) setRandomTopic(result.topic)
|
if (result?.topic) setRandomTopic(result.topic)
|
||||||
if (result?.shouts) setRandomTopicArticles(result.shouts)
|
if (result?.shouts) setRandomTopicArticles(result.shouts)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -29,12 +29,9 @@ export const ProfileSubscriptions = () => {
|
||||||
const fetchSubscriptions = async () => {
|
const fetchSubscriptions = async () => {
|
||||||
try {
|
try {
|
||||||
const slug = author()?.slug
|
const slug = author()?.slug
|
||||||
const [getAuthors, getTopics] = await Promise.all([
|
const authorFollows = await apiClient.getAuthorFollows({ slug })
|
||||||
apiClient.getAuthorFollowingAuthors({ slug }),
|
setFollowing([...authorFollows['authors']])
|
||||||
apiClient.getAuthorFollowingTopics({ slug }),
|
setFiltered([...authorFollows['authors'], ...authorFollows['topics']])
|
||||||
])
|
|
||||||
setFollowing([...getAuthors, ...getTopics])
|
|
||||||
setFiltered([...getAuthors, ...getTopics])
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[fetchSubscriptions] :', error)
|
console.error('[fetchSubscriptions] :', error)
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -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 {
|
.teaser {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { Button } from '../Button'
|
||||||
import { DropdownSelect } from '../DropdownSelect'
|
import { DropdownSelect } from '../DropdownSelect'
|
||||||
import { Loading } from '../Loading'
|
import { Loading } from '../Loading'
|
||||||
|
|
||||||
|
import { InlineLoader } from '../../InlineLoader'
|
||||||
import styles from './InviteMembers.module.scss'
|
import styles from './InviteMembers.module.scss'
|
||||||
|
|
||||||
type InviteAuthor = Author & { selected: boolean }
|
type InviteAuthor = Author & { selected: boolean }
|
||||||
|
@ -62,7 +63,7 @@ export const InviteMembers = (props: Props) => {
|
||||||
return authors?.slice(start, end)
|
return authors?.slice(start, end)
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pages, _infiniteScrollLoader, { end }] = createInfiniteScroll(fetcher)
|
const [pages, setEl, { end }] = createInfiniteScroll(fetcher)
|
||||||
|
|
||||||
createEffect(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
|
@ -158,11 +159,8 @@ export const InviteMembers = (props: Props) => {
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Show when={!end()}>
|
<Show when={!end()}>
|
||||||
<div use:infiniteScrollLoader class={styles.loading}>
|
<div ref={setEl as (e: HTMLDivElement) => void}>
|
||||||
<div class={styles.icon}>
|
<InlineLoader />
|
||||||
<Loading size="tiny" />
|
|
||||||
</div>
|
|
||||||
<div>{t('Loading')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,20 +2,14 @@ import { Accessor, JSX, createContext, createEffect, createSignal, useContext }
|
||||||
import { createStore } from 'solid-js/store'
|
import { createStore } from 'solid-js/store'
|
||||||
|
|
||||||
import { apiClient } from '../graphql/client/core'
|
import { apiClient } from '../graphql/client/core'
|
||||||
import { Author, Community, FollowingEntity, Topic } from '../graphql/schema/core.gen'
|
import { AuthorFollows, FollowingEntity } from '../graphql/schema/core.gen'
|
||||||
|
|
||||||
import { useSession } from './session'
|
import { useSession } from './session'
|
||||||
|
|
||||||
type SubscriptionsData = {
|
|
||||||
topics?: Topic[]
|
|
||||||
authors?: Author[]
|
|
||||||
communities?: Community[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FollowingContextType {
|
interface FollowingContextType {
|
||||||
loading: Accessor<boolean>
|
loading: Accessor<boolean>
|
||||||
subscriptions: SubscriptionsData
|
subscriptions: AuthorFollows
|
||||||
setSubscriptions: (subscriptions: SubscriptionsData) => void
|
setSubscriptions: (subscriptions: AuthorFollows) => void
|
||||||
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
setFollowing: (what: FollowingEntity, slug: string, value: boolean) => void
|
||||||
loadSubscriptions: () => void
|
loadSubscriptions: () => void
|
||||||
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
follow: (what: FollowingEntity, slug: string) => Promise<void>
|
||||||
|
@ -29,7 +23,7 @@ export function useFollowing() {
|
||||||
return useContext(FollowingContext)
|
return useContext(FollowingContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
const EMPTY_SUBSCRIPTIONS = {
|
const EMPTY_SUBSCRIPTIONS: AuthorFollows = {
|
||||||
topics: [],
|
topics: [],
|
||||||
authors: [],
|
authors: [],
|
||||||
communities: [],
|
communities: [],
|
||||||
|
@ -37,15 +31,15 @@ const EMPTY_SUBSCRIPTIONS = {
|
||||||
|
|
||||||
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
export const FollowingProvider = (props: { children: JSX.Element }) => {
|
||||||
const [loading, setLoading] = createSignal<boolean>(false)
|
const [loading, setLoading] = createSignal<boolean>(false)
|
||||||
const [subscriptions, setSubscriptions] = createStore<SubscriptionsData>(EMPTY_SUBSCRIPTIONS)
|
const [subscriptions, setSubscriptions] = createStore<AuthorFollows>(EMPTY_SUBSCRIPTIONS)
|
||||||
const { author } = useSession()
|
const { author, session } = useSession()
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
if (apiClient.private) {
|
if (apiClient.private) {
|
||||||
console.debug('[context.following] fetching subs data...')
|
console.debug('[context.following] fetching subs data...')
|
||||||
const result = await apiClient.getMySubscriptions()
|
const result = await apiClient.getAuthorFollows({ user: session()?.user.id })
|
||||||
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
setSubscriptions(result || EMPTY_SUBSCRIPTIONS)
|
||||||
console.info('[context.following] subs:', subscriptions)
|
console.info('[context.following] subs:', subscriptions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ type ReactionsContextType = {
|
||||||
offset?: number
|
offset?: number
|
||||||
}) => Promise<Reaction[]>
|
}) => Promise<Reaction[]>
|
||||||
createReaction: (reaction: ReactionInput) => Promise<void>
|
createReaction: (reaction: ReactionInput) => Promise<void>
|
||||||
updateReaction: (id: number, reaction: ReactionInput) => Promise<void>
|
updateReaction: (reaction: ReactionInput) => Promise<void>
|
||||||
deleteReaction: (id: number) => Promise<void>
|
deleteReaction: (id: number) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,8 +88,8 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateReaction = async (id: number, input: ReactionInput): Promise<void> => {
|
const updateReaction = async (input: ReactionInput): Promise<void> => {
|
||||||
const reaction = await apiClient.updateReaction(id, input)
|
const reaction = await apiClient.updateReaction(input)
|
||||||
setReactionEntities(reaction.id, reaction)
|
setReactionEntities(reaction.id, reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type {
|
import type {
|
||||||
Author,
|
Author,
|
||||||
|
AuthorFollows,
|
||||||
CommonResult,
|
CommonResult,
|
||||||
Community,
|
|
||||||
FollowingEntity,
|
FollowingEntity,
|
||||||
LoadShoutsOptions,
|
LoadShoutsOptions,
|
||||||
MutationDelete_ShoutArgs,
|
MutationDelete_ShoutArgs,
|
||||||
|
@ -37,16 +37,14 @@ import shoutsLoadSearch from '../query/core/articles-load-search'
|
||||||
import loadShoutsUnrated from '../query/core/articles-load-unrated'
|
import loadShoutsUnrated from '../query/core/articles-load-unrated'
|
||||||
import authorBy from '../query/core/author-by'
|
import authorBy from '../query/core/author-by'
|
||||||
import authorFollowers from '../query/core/author-followers'
|
import authorFollowers from '../query/core/author-followers'
|
||||||
|
import authorFollows from '../query/core/author-follows'
|
||||||
import authorId from '../query/core/author-id'
|
import authorId from '../query/core/author-id'
|
||||||
import authorsAll from '../query/core/authors-all'
|
import authorsAll from '../query/core/authors-all'
|
||||||
import authorFollowedAuthors from '../query/core/authors-followed-by'
|
|
||||||
import authorsLoadBy from '../query/core/authors-load-by'
|
import authorsLoadBy from '../query/core/authors-load-by'
|
||||||
import authorFollowedCommunities from '../query/core/communities-followed-by'
|
|
||||||
import mySubscriptions from '../query/core/my-followed'
|
import mySubscriptions from '../query/core/my-followed'
|
||||||
import reactionsLoadBy from '../query/core/reactions-load-by'
|
import reactionsLoadBy from '../query/core/reactions-load-by'
|
||||||
import topicBySlug from '../query/core/topic-by-slug'
|
import topicBySlug from '../query/core/topic-by-slug'
|
||||||
import topicsAll from '../query/core/topics-all'
|
import topicsAll from '../query/core/topics-all'
|
||||||
import authorFollowedTopics from '../query/core/topics-followed-by'
|
|
||||||
import topicsRandomQuery from '../query/core/topics-random'
|
import topicsRandomQuery from '../query/core/topics-random'
|
||||||
|
|
||||||
const publicGraphQLClient = createGraphQLClient('core')
|
const publicGraphQLClient = createGraphQLClient('core')
|
||||||
|
@ -86,7 +84,7 @@ export const apiClient = {
|
||||||
return response.data.get_topics_random
|
return response.data.get_topics_random
|
||||||
},
|
},
|
||||||
|
|
||||||
getRandomTopicShouts: async (limit: number): Promise<{ topic: Topic; shouts: Shout[] }> => {
|
getRandomTopicShouts: async (limit: number): Promise<CommonResult> => {
|
||||||
const resp = await publicGraphQLClient.query(articlesLoadRandomTopic, { limit }).toPromise()
|
const resp = await publicGraphQLClient.query(articlesLoadRandomTopic, { limit }).toPromise()
|
||||||
if (!resp.data) console.error('[graphql.client.core] load_shouts_random_topic', resp)
|
if (!resp.data) console.error('[graphql.client.core] load_shouts_random_topic', resp)
|
||||||
return resp.data.load_shouts_random_topic
|
return resp.data.load_shouts_random_topic
|
||||||
|
@ -96,6 +94,7 @@ export const apiClient = {
|
||||||
const response = await apiClient.private.mutation(followMutation, { what, slug }).toPromise()
|
const response = await apiClient.private.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 apiClient.private.mutation(unfollowMutation, { what, slug }).toPromise()
|
const response = await apiClient.private.mutation(unfollowMutation, { what, slug }).toPromise()
|
||||||
return response.data.unfollow
|
return response.data.unfollow
|
||||||
|
@ -107,48 +106,53 @@ export const apiClient = {
|
||||||
|
|
||||||
return response.data.get_topics_all
|
return response.data.get_topics_all
|
||||||
},
|
},
|
||||||
|
|
||||||
getAllAuthors: async () => {
|
getAllAuthors: async () => {
|
||||||
const response = await publicGraphQLClient.query(authorsAll, {}).toPromise()
|
const response = await publicGraphQLClient.query(authorsAll, {}).toPromise()
|
||||||
if (!response.data) console.error('[graphql.client.core] getAllAuthors', response)
|
if (!response.data) console.error('[graphql.client.core] getAllAuthors', response)
|
||||||
|
|
||||||
return response.data.get_authors_all
|
return response.data.get_authors_all
|
||||||
},
|
},
|
||||||
|
|
||||||
getAuthor: async (params: { slug?: string; author_id?: number }): Promise<Author> => {
|
getAuthor: async (params: { slug?: string; author_id?: number }): Promise<Author> => {
|
||||||
const response = await publicGraphQLClient.query(authorBy, params).toPromise()
|
const response = await publicGraphQLClient.query(authorBy, params).toPromise()
|
||||||
return response.data.get_author
|
return response.data.get_author
|
||||||
},
|
},
|
||||||
|
|
||||||
getAuthorId: async (params: { user: string }): Promise<Author> => {
|
getAuthorId: async (params: { user: string }): Promise<Author> => {
|
||||||
const response = await publicGraphQLClient.query(authorId, params).toPromise()
|
const response = await publicGraphQLClient.query(authorId, params).toPromise()
|
||||||
return response.data.get_author_id
|
return response.data.get_author_id
|
||||||
},
|
},
|
||||||
|
|
||||||
getAuthorFollowers: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
getAuthorFollowers: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
||||||
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(authorFollowers, { slug }).toPromise()
|
||||||
return response.data.get_author_followers
|
return response.data.get_author_followers
|
||||||
},
|
},
|
||||||
getAuthorFollowingAuthors: async ({ slug }: { slug: string }): Promise<Author[]> => {
|
|
||||||
const response = await publicGraphQLClient.query(authorFollowedAuthors, { slug }).toPromise()
|
getAuthorFollows: async (params: {
|
||||||
return response.data.get_author_followed
|
slug?: string
|
||||||
},
|
author_id?: number
|
||||||
getAuthorFollowingTopics: async ({ slug }: { slug: string }): Promise<Topic[]> => {
|
user?: string
|
||||||
const response = await publicGraphQLClient.query(authorFollowedTopics, { slug }).toPromise()
|
}): Promise<AuthorFollows> => {
|
||||||
return response.data.get_topics_by_author
|
const response = await publicGraphQLClient.query(authorFollows, params).toPromise()
|
||||||
},
|
return response.data.get_author_follows
|
||||||
getAuthorFollowingCommunities: async ({ slug }: { slug: string }): Promise<Community[]> => {
|
|
||||||
const response = await publicGraphQLClient.query(authorFollowedCommunities, { slug }).toPromise()
|
|
||||||
return response.data.get_communities_by_author
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateAuthor: async (input: ProfileInput) => {
|
updateAuthor: async (input: ProfileInput) => {
|
||||||
const response = await apiClient.private.mutation(updateAuthor, { profile: input }).toPromise()
|
const response = await apiClient.private.mutation(updateAuthor, { profile: input }).toPromise()
|
||||||
return response.data.update_author
|
return response.data.update_author
|
||||||
},
|
},
|
||||||
|
|
||||||
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
|
getTopic: async ({ slug }: { slug: string }): Promise<Topic> => {
|
||||||
const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise()
|
const response = await publicGraphQLClient.query(topicBySlug, { slug }).toPromise()
|
||||||
return response.data.get_topic
|
return response.data.get_topic
|
||||||
},
|
},
|
||||||
|
|
||||||
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
|
createArticle: async ({ article }: { article: ShoutInput }): Promise<Shout> => {
|
||||||
const response = await apiClient.private.mutation(createArticle, { shout: article }).toPromise()
|
const response = await apiClient.private.mutation(createArticle, { shout: article }).toPromise()
|
||||||
return response.data.create_shout.shout
|
return response.data.create_shout.shout
|
||||||
},
|
},
|
||||||
|
|
||||||
updateArticle: async ({
|
updateArticle: async ({
|
||||||
shout_id,
|
shout_id,
|
||||||
shout_input,
|
shout_input,
|
||||||
|
@ -164,10 +168,12 @@ export const apiClient = {
|
||||||
console.debug('[graphql.client.core] updateArticle:', response.data)
|
console.debug('[graphql.client.core] updateArticle:', response.data)
|
||||||
return response.data.update_shout.shout
|
return response.data.update_shout.shout
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteShout: async (params: MutationDelete_ShoutArgs): Promise<void> => {
|
deleteShout: async (params: MutationDelete_ShoutArgs): Promise<void> => {
|
||||||
const response = await apiClient.private.mutation(deleteShout, params).toPromise()
|
const response = await apiClient.private.mutation(deleteShout, params).toPromise()
|
||||||
console.debug('[graphql.client.core] deleteShout:', response)
|
console.debug('[graphql.client.core] deleteShout:', response)
|
||||||
},
|
},
|
||||||
|
|
||||||
getDrafts: async (): Promise<Shout[]> => {
|
getDrafts: async (): Promise<Shout[]> => {
|
||||||
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
const response = await apiClient.private.query(draftsLoad, {}).toPromise()
|
||||||
console.debug('[graphql.client.core] getDrafts:', response)
|
console.debug('[graphql.client.core] getDrafts:', response)
|
||||||
|
@ -178,15 +184,13 @@ export const apiClient = {
|
||||||
console.debug('[graphql.client.core] createReaction:', response)
|
console.debug('[graphql.client.core] createReaction:', response)
|
||||||
return response.data.create_reaction.reaction
|
return response.data.create_reaction.reaction
|
||||||
},
|
},
|
||||||
destroyReaction: async (id: number) => {
|
destroyReaction: async (reaction_id: number) => {
|
||||||
const response = await apiClient.private.mutation(reactionDestroy, { id: id }).toPromise()
|
const response = await apiClient.private.mutation(reactionDestroy, { reaction_id }).toPromise()
|
||||||
console.debug('[graphql.client.core] destroyReaction:', response)
|
console.debug('[graphql.client.core] destroyReaction:', response)
|
||||||
return response.data.delete_reaction.reaction
|
return response.data.delete_reaction.reaction
|
||||||
},
|
},
|
||||||
updateReaction: async (id: number, input: ReactionInput) => {
|
updateReaction: async (reaction: ReactionInput) => {
|
||||||
const response = await apiClient.private
|
const response = await apiClient.private.mutation(reactionUpdate, { reaction }).toPromise()
|
||||||
.mutation(reactionUpdate, { id: id, reaction: input })
|
|
||||||
.toPromise()
|
|
||||||
console.debug('[graphql.client.core] updateReaction:', response)
|
console.debug('[graphql.client.core] updateReaction:', response)
|
||||||
return response.data.update_reaction.reaction
|
return response.data.update_reaction.reaction
|
||||||
},
|
},
|
||||||
|
@ -233,9 +237,4 @@ export const apiClient = {
|
||||||
.toPromise()
|
.toPromise()
|
||||||
return resp.data.load_reactions_by
|
return resp.data.load_reactions_by
|
||||||
},
|
},
|
||||||
getMySubscriptions: async (): Promise<CommonResult> => {
|
|
||||||
const resp = await apiClient.private.query(mySubscriptions, {}).toPromise()
|
|
||||||
|
|
||||||
return resp.data.get_my_followed
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { gql } from '@urql/core'
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
export default gql`
|
export default gql`
|
||||||
mutation UpdateReactionMutation($id: Int!, $reaction: ReactionInput!) {
|
mutation UpdateReactionMutation($reaction: ReactionInput!) {
|
||||||
update_reaction(id: $id, reaction: $reaction) {
|
update_reaction(reaction: $reaction) {
|
||||||
error
|
error
|
||||||
reaction {
|
reaction {
|
||||||
id
|
id
|
||||||
|
|
20
src/graphql/query/core/author-follows.ts
Normal file
20
src/graphql/query/core/author-follows.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { gql } from '@urql/core'
|
||||||
|
|
||||||
|
export default gql`
|
||||||
|
query GetAuthorFollows($slug: String, $user: String, $author_id: Int) {
|
||||||
|
get_author_follows(slug: $slug, user: $user, author_id: $author_id) {
|
||||||
|
authors {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
name
|
||||||
|
pic
|
||||||
|
bio
|
||||||
|
}
|
||||||
|
topics {
|
||||||
|
id
|
||||||
|
slug
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
|
@ -1,17 +0,0 @@
|
||||||
import { gql } from '@urql/core'
|
|
||||||
|
|
||||||
export default gql`
|
|
||||||
query AuthorsFollowedByQuery($slug: String, $user: String, $author_id: Int) {
|
|
||||||
get_author_followed(slug: $slug, user: $user, author_id: $author_id) {
|
|
||||||
id
|
|
||||||
slug
|
|
||||||
name
|
|
||||||
pic
|
|
||||||
bio
|
|
||||||
created_at
|
|
||||||
stat {
|
|
||||||
shouts
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { PageProps } from './types'
|
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 { PageLayout } from '../components/_shared/PageLayout'
|
||||||
import { useLocalize } from '../context/localize'
|
import { useLocalize } from '../context/localize'
|
||||||
import { loadAllAuthors } from '../stores/zine/authors'
|
import { loadAllAuthors } from '../stores/zine/authors'
|
||||||
|
@ -23,7 +23,7 @@ export const AllAuthorsPage = (props: PageProps) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title={t('Authors')}>
|
<PageLayout title={t('Authors')}>
|
||||||
<AllAuthorsView isLoaded={isLoaded()} authors={props.allAuthors} />
|
<AllAuthors isLoaded={isLoaded()} authors={props.allAuthors} />
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -153,7 +153,7 @@ export const useRouter = <TSearchParams extends Record<string, string> = Record<
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearSearchParams = (replace = false) => {
|
const clearSearchParams = (replace = false) => {
|
||||||
searchParamsStore.open({}, replace)
|
// searchParamsStore.open({}, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Author, QueryLoad_Authors_ByArgs } from '../../graphql/schema/core.gen'
|
||||||
import { byStat } from '../../utils/sortby'
|
import { byStat } from '../../utils/sortby'
|
||||||
|
|
||||||
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
export type AuthorsSortBy = 'shouts' | 'name' | 'followers'
|
||||||
|
type SortedAuthorsSetter = (prev: Author[]) => Author[]
|
||||||
|
|
||||||
const [sortAllBy, setSortAllBy] = createSignal<AuthorsSortBy>('name')
|
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 [authorEntities, setAuthorEntities] = createSignal<{ [authorSlug: string]: Author }>({})
|
||||||
const [authorsByTopic, setAuthorsByTopic] = createSignal<{ [topicSlug: 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 sortedAuthors = createLazyMemo(() => {
|
||||||
const authors = Object.values(authorEntities())
|
const authors = Object.values(authorEntities())
|
||||||
|
@ -108,5 +114,5 @@ export const useAuthorsStore = (initialState: InitialState = {}) => {
|
||||||
}
|
}
|
||||||
addAuthors([...(initialState.authors || [])])
|
addAuthors([...(initialState.authors || [])])
|
||||||
|
|
||||||
return { authorEntities, sortedAuthors, authorsByTopic }
|
return { authorEntities, sortedAuthors, authorsByTopic, authorsByShouts, authorsByFollowers }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user