author-shouts-loadmore

This commit is contained in:
Untone 2024-09-03 11:07:32 +03:00
parent 33a81d8ee7
commit e176544e36
3 changed files with 63 additions and 51 deletions

View File

@ -62,8 +62,8 @@ const getTitleAndSubtitle = (
title: string title: string
subtitle: string subtitle: string
} => { } => {
let title = article?.title || '' let title = article.title || ''
let subtitle: string = article?.subtitle || '' let subtitle: string = article.subtitle || ''
if (!subtitle) { if (!subtitle) {
let titleParts = article.title?.split('. ') || [] let titleParts = article.title?.split('. ') || []
@ -85,8 +85,8 @@ const getTitleAndSubtitle = (
} }
const getMainTopicTitle = (article: Shout, lng: string) => { const getMainTopicTitle = (article: Shout, lng: string) => {
const mainTopicSlug = article?.main_topic || '' const mainTopicSlug = article.main_topic || ''
const mainTopic = (article?.topics || []).find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug) const mainTopic = (article.topics || []).find((tpc: Maybe<Topic>) => tpc?.slug === mainTopicSlug)
const mainTopicTitle = const mainTopicTitle =
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || '' mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
@ -109,29 +109,30 @@ export const ArticleCard = (props: ArticleCardProps) => {
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false) const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true) const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = descFromBody(props.article?.body) const description = descFromBody(props.article.body)
const aspectRatio: Accessor<string> = () => LAYOUT_ASPECT[props.article?.layout as string] const aspectRatio: Accessor<string> = () => LAYOUT_ASPECT[props.article.layout as string]
const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang()) const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang())
const { title, subtitle } = getTitleAndSubtitle(props.article) const { title, subtitle } = getTitleAndSubtitle(props.article)
const formattedDate = createMemo<string>(() => const formattedDate = createMemo<string>(() =>
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 = createMemo( const canEdit = createMemo(
() => () =>
Boolean(author()?.id) && Boolean(author()?.id) &&
(props.article?.authors?.some((a) => Boolean(a) && a?.id === author().id) || (props.article.authors?.some((a) => Boolean(a) && a?.id === author().id) ||
props.article?.created_by?.id === author().id || props.article.created_by?.id === author().id ||
session()?.user?.roles?.includes('editor')) session()?.user?.roles?.includes('editor'))
) )
const navigate = useNavigate() const navigate = useNavigate()
const scrollToComments = (event: MouseEvent & { currentTarget: HTMLAnchorElement; target: Element }) => { const scrollToComments = (event: MouseEvent & { currentTarget: HTMLAnchorElement; target: Element }) => {
event.preventDefault() event.preventDefault()
navigate(`/${props.article.slug}`)
changeSearchParams({ changeSearchParams({
scrollTo: 'comments' scrollTo: 'comments'
}) })
navigate(`/${props.article.slug}`)
} }
const onInvite = () => { const onInvite = () => {
@ -196,10 +197,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
} }
> >
<div class={styles.shoutCardType}> <div class={styles.shoutCardType}>
<a href={`/expo/${props.article.layout}`}> <A href={`/expo/${props.article.layout}`}>
<Icon name={props.article.layout} class={styles.icon} /> <Icon name={props.article.layout} class={styles.icon} />
{/*<Icon name={`${layout}-hover`} class={clsx(styles.icon, styles.iconHover)} />*/} </A>
</a>
</div> </div>
</Show> </Show>
@ -220,7 +220,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode [styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
})} })}
> >
<A href={`/${props.article?.slug || ''}`}> <A href={`/${props.article.slug}`}>
<div class={styles.shoutCardTitle}> <div class={styles.shoutCardTitle}>
<span class={styles.shoutCardLinkWrapper}> <span class={styles.shoutCardLinkWrapper}>
<span class={styles.shoutCardLinkContainer} innerHTML={title} /> <span class={styles.shoutCardLinkContainer} innerHTML={title} />
@ -280,10 +280,9 @@ export const ArticleCard = (props: ArticleCardProps) => {
} }
> >
<div class={styles.shoutCardType}> <div class={styles.shoutCardType}>
<a href={`/expo/${props.article.layout}`}> <A href={`/expo/${props.article.layout}`}>
<Icon name={props.article.layout} class={styles.icon} /> <Icon name={props.article.layout} class={styles.icon} />
{/*<Icon name={`${layout}-hover`} class={clsx(styles.icon, styles.iconHover)} />*/} </A>
</a>
</div> </div>
</Show> </Show>
<div class={styles.shoutCardCover}> <div class={styles.shoutCardCover}>
@ -332,7 +331,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<Popover content={t('Edit')} disabled={isActionPopupActive()}> <Popover content={t('Edit')} disabled={isActionPopupActive()}>
{(triggerRef: (el: HTMLElement) => void) => ( {(triggerRef: (el: HTMLElement) => void) => (
<div class={styles.shoutCardDetailsItem} ref={triggerRef}> <div class={styles.shoutCardDetailsItem} ref={triggerRef}>
<A href={`/edit/${props.article?.id}`}> <A href={`/edit/${props.article.id}`}>
<Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} /> <Icon name="pencil-outline" class={clsx(styles.icon, styles.feedControlIcon)} />
<Icon <Icon
name="pencil-outline-hover" name="pencil-outline-hover"

View File

@ -1,16 +1,20 @@
import { A, useLocation, useParams } from '@solidjs/router' import { A, useLocation, useParams } from '@solidjs/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal } from 'solid-js' import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { coreApiUrl } from '~/config' import { coreApiUrl } from '~/config'
import { useAuthors } from '~/context/authors' import { useAuthors } from '~/context/authors'
import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed'
import { useFollowing } from '~/context/following' import { useFollowing } from '~/context/following'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { useSession } from '~/context/session' import { useSession } from '~/context/session'
import { loadShouts } from '~/graphql/api/public'
import { graphqlClientCreate } from '~/graphql/client' import { graphqlClientCreate } from '~/graphql/client'
import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers'
import getAuthorFollowsQuery from '~/graphql/query/core/author-follows' import getAuthorFollowsQuery from '~/graphql/query/core/author-follows'
import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen' import type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { byCreated } from '~/utils/sort' import { byCreated } from '~/utils/sort'
import stylesArticle from '../../Article/Article.module.scss' import stylesArticle from '../../Article/Article.module.scss'
import { Comment } from '../../Article/Comment' import { Comment } from '../../Article/Comment'
@ -131,6 +135,32 @@ export const AuthorView = (props: AuthorViewProps) => {
</div> </div>
) )
const { feedByAuthor, addFeed } = useFeed()
const [sortedFeed, setSortedFeed] = createSignal<Shout[]>([])
const [loadMoreHidden, setLoadMoreHidden] = createSignal(false)
const loadMore = async () => {
saveScrollPosition()
const amountBefore = feedByAuthor()?.[props.authorSlug]?.length || 0
const topicShoutsFetcher = loadShouts({
filters: { author: props.authorSlug },
limit: SHOUTS_PER_PAGE,
offset: amountBefore
})
const result = await topicShoutsFetcher()
result && addFeed(result)
const amountAfter = feedByAuthor()?.[props.authorSlug].length
setLoadMoreHidden(amountAfter === amountBefore)
restoreScrollPosition()
return result as LoadMoreItems
}
// fx to update author's feed
createEffect(on(feedByAuthor, (byAuthor) => {
const feed = byAuthor[props.authorSlug] as Shout[]
if (!feed) return
setSortedFeed(feed)
},{}))
return ( return (
<div class={styles.authorPage}> <div class={styles.authorPage}>
<div class="wide-container"> <div class="wide-container">
@ -218,10 +248,14 @@ export const AuthorView = (props: AuthorViewProps) => {
</div> </div>
</Show> </Show>
<Show when={Array.isArray(props.shouts) && props.shouts.length > 0}> <LoadMoreWrapper loadFunction={loadMore} pageSize={SHOUTS_PER_PAGE} hidden={loadMoreHidden()}>
<For each={props.shouts.filter((_, i) => i % 3 === 0)}> <For
each={sortedFeed()
.filter((_, i) => i % 3 === 0)}
>
{(_shout, index) => { {(_shout, index) => {
const articles = props.shouts.slice(index() * 3, index() * 3 + 3) const articles = sortedFeed()
.slice(index() * 3, index() * 3 + 3)
return ( return (
<> <>
<Switch> <Switch>
@ -239,7 +273,7 @@ export const AuthorView = (props: AuthorViewProps) => {
) )
}} }}
</For> </For>
</Show> </LoadMoreWrapper>
</Match> </Match>
</Switch> </Switch>
</div> </div>

View File

@ -2,11 +2,10 @@ import { RouteSectionProps, createAsync } from '@solidjs/router'
import { ErrorBoundary, Suspense, createEffect, createSignal, on } from 'solid-js' import { ErrorBoundary, Suspense, createEffect, createSignal, on } from 'solid-js'
import { AuthorView } from '~/components/Views/Author' import { AuthorView } from '~/components/Views/Author'
import { FourOuFourView } from '~/components/Views/FourOuFour' import { FourOuFourView } from '~/components/Views/FourOuFour'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { useAuthors } from '~/context/authors' import { useAuthors } from '~/context/authors'
import { SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { SHOUTS_PER_PAGE } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { ReactionsProvider } from '~/context/reactions' import { ReactionsProvider } from '~/context/reactions'
import { loadAuthors, loadShouts, loadTopics } from '~/graphql/api/public' import { loadAuthors, loadShouts, loadTopics } from '~/graphql/api/public'
@ -106,21 +105,7 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
) )
// author's shouts // author's shouts
const { addFeed, feedByAuthor } = useFeed() const authorShouts = createAsync(async () => props.data.articles as Shout[] || await fetchAuthorShouts(props.params.slug, 0))
const [loadMoreHidden, setLoadMoreHidden] = createSignal(true)
const authorShouts = createAsync(async () => {
const sss: Shout[] = (props.data.articles as Shout[]) || feedByAuthor()[props.params.slug] || []
const result = sss || (await fetchAuthorShouts(props.params.slug, 0))
if (!result) setLoadMoreHidden(true)
return result
})
// load more shouts
const loadAuthorShoutsMore = async (offset: number) => {
const loadedShouts = await fetchAuthorShouts(props.params.slug, offset)
loadedShouts && addFeed(loadedShouts)
return (loadedShouts || []) as LoadMoreItems
}
return ( return (
<ErrorBoundary fallback={(_err) => <FourOuFourView />}> <ErrorBoundary fallback={(_err) => <FourOuFourView />}>
@ -133,17 +118,11 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
cover={cover()} cover={cover()}
> >
<ReactionsProvider> <ReactionsProvider>
<LoadMoreWrapper
loadFunction={loadAuthorShoutsMore}
pageSize={SHOUTS_PER_PAGE}
hidden={loadMoreHidden()}
>
<AuthorView <AuthorView
author={author() as Author} author={author() as Author}
authorSlug={props.params.slug} authorSlug={props.params.slug}
shouts={authorShouts() || []} shouts={authorShouts() || []}
/> />
</LoadMoreWrapper>
</ReactionsProvider> </ReactionsProvider>
</PageLayout> </PageLayout>
</Suspense> </Suspense>