webapp/src/components/Views/Author/Author.tsx
2024-06-26 11:22:05 +03:00

323 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Meta, Title } from '@solidjs/meta'
import { A, useLocation, useMatch } from '@solidjs/router'
import { clsx } from 'clsx'
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { useAuthors } from '~/context/authors'
import { useGraphQL } from '~/context/graphql'
import { useUI } from '~/context/ui'
import { useFeed } from '../../../context/feed'
import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import loadShoutsQuery from '../../../graphql/query/core/articles-load-by'
import getAuthorFollowersQuery from '../../../graphql/query/core/author-followers'
import getAuthorFollowsQuery from '../../../graphql/query/core/author-follows'
import loadReactionsBy from '../../../graphql/query/core/reactions-load-by'
import type { Author, Reaction, Shout, Topic } from '../../../graphql/schema/core.gen'
import { getImageUrl } from '../../../utils/getImageUrl'
import { getDescription } from '../../../utils/meta'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { byCreated } from '../../../utils/sortby'
import { splitToPages } from '../../../utils/splitToPages'
import stylesArticle from '../../Article/Article.module.scss'
import { Comment } from '../../Article/Comment'
import { AuthorCard } from '../../Author/AuthorCard'
import { AuthorShoutsRating } from '../../Author/AuthorShoutsRating'
import { Placeholder } from '../../Feed/Placeholder'
import { Row1 } from '../../Feed/Row1'
import { Row2 } from '../../Feed/Row2'
import { Row3 } from '../../Feed/Row3'
import { Loading } from '../../_shared/Loading'
import styles from './Author.module.scss'
type Props = {
authorSlug: string
shouts?: Shout[]
author?: Author
}
export const PRERENDERED_ARTICLES_COUNT = 12
const LOAD_MORE_PAGE_SIZE = 9
export const AuthorView = (props: Props) => {
const { t } = useLocalize()
const { followers: myFollowers, follows: myFollows } = useFollowing()
const { session } = useSession()
const me = createMemo<Author>(() => session()?.user?.app_data?.profile as Author)
const [slug, setSlug] = createSignal(props.authorSlug)
const { sortedFeed } = useFeed()
const { modal, hideModal } = useUI()
const loc = useLocation()
const matchAuthor = useMatch(() => '/author')
const matchComments = useMatch(() => '/author/:authorId/comments')
const matchAbout = useMatch(() => '/author/:authorId/about')
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
const { loadAuthor, authorsEntities } = useAuthors()
const [author, setAuthor] = createSignal<Author>()
const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
const [following, changeFollowing] = createSignal<Array<Author | Topic>>([] as Array<Author | Topic>) // flat AuthorFollowsResult
const [showExpandBioControl, setShowExpandBioControl] = createSignal(false)
const [commented, setCommented] = createSignal<Reaction[]>()
const { query } = useGraphQL()
// пагинация загрузки ленты постов
const loadMore = async () => {
saveScrollPosition()
const resp = await query(loadShoutsQuery, {
filters: { author: props.authorSlug },
limit: LOAD_MORE_PAGE_SIZE,
offset: sortedFeed().length
})
const hasMore = resp?.data?.load_shouts_by?.hasMore
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition()
}
// 1 // проверяет не собственный ли это профиль, иначе - загружает
const [isFetching, setIsFetching] = createSignal(false)
createEffect(
on([() => session()?.user?.app_data?.profile, () => props.authorSlug || ''], async ([me, s]) => {
const my = s && me?.slug === s
if (my) {
console.debug('[Author] my profile precached')
if (me) {
setAuthor(me)
if (myFollowers()) setFollowers((myFollowers() || []) as Author[])
changeFollowing([...(myFollows?.topics || []), ...(myFollows?.authors || [])])
}
} else if (s && !isFetching()) {
setIsFetching(true)
setSlug(s)
await loadAuthor(s)
setIsFetching(false) // Сброс состояния загрузки после завершения
}
})
)
// 3 // after fetch loading following data
createEffect(
on(
[followers, () => authorsEntities()[slug()]],
async ([current, found]) => {
if (current) return
if (!found) return
setAuthor(found)
console.info(`[Author] profile for @${slug()} fetched`)
const followsResp = await query(getAuthorFollowsQuery, { slug: slug() }).toPromise()
const follows = followsResp?.data?.get_author_followers || {}
changeFollowing([...(follows?.authors || []), ...(follows?.topics || [])])
console.info(`[Author] follows for @${slug()} fetched`)
const followersResp = await query(getAuthorFollowersQuery, { slug: slug() }).toPromise()
setFollowers(followersResp?.data?.get_author_followers || [])
console.info(`[Author] followers for @${slug()} fetched`)
setIsFetching(false)
},
{ defer: true }
)
)
// догружает ленту и комментарии
createEffect(
on(
() => author() as Author,
async (profile: Author) => {
if (!commented() && profile) {
await loadMore()
const resp = await query(loadReactionsBy, {
by: { comment: true, created_by: profile.id }
}).toPromise()
const ccc = resp?.data?.load_reactions_by
if (ccc) setCommented(ccc)
}
}
// { defer: true },
)
)
let bioContainerRef: HTMLDivElement
let bioWrapperRef: HTMLDivElement
const checkBioHeight = () => {
if (bioContainerRef) {
setShowExpandBioControl(bioContainerRef.offsetHeight > bioWrapperRef.offsetHeight)
}
}
onMount(() => {
if (!modal()) hideModal()
checkBioHeight()
})
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
const ogImage = createMemo(() =>
author()?.pic
? getImageUrl(author()?.pic || '', { width: 1200 })
: getImageUrl('production/image/logo_image.png')
)
const description = createMemo(() => getDescription(author()?.bio || ''))
const handleDeleteComment = (id: number) => {
setCommented((prev) => (prev || []).filter((comment) => comment.id !== id))
}
return (
<div class={styles.authorPage}>
<Show when={author()}>
<Title>{author()?.name}</Title>
<Meta name="descprition" content={description()} />
<Meta name="og:type" content="profile" />
<Meta name="og:title" content={author()?.name || ''} />
<Meta name="og:image" content={ogImage()} />
<Meta name="og:description" content={description()} />
<Meta name="twitter:card" content="summary_large_image" />
<Meta name="twitter:title" content={author()?.name || ''} />
<Meta name="twitter:description" content={description()} />
<Meta name="twitter:image" content={ogImage()} />
</Show>
<div class="wide-container">
<Show when={author()} fallback={<Loading />}>
<>
<div class={styles.authorHeader}>
<AuthorCard
author={author() as Author}
followers={followers() || []}
flatFollows={following() || []}
/>
</div>
<div class={clsx(styles.groupControls, 'row')}>
<div class="col-md-16">
<ul class="view-switcher">
<li classList={{ 'view-switcher__item--selected': !!matchAuthor() }}>
<A href={`/author/${props.authorSlug}`}>{t('Publications')}</A>
<Show when={author()?.stat}>
<span class="view-switcher__counter">{author()?.stat?.shouts || 0}</span>
</Show>
</li>
<li classList={{ 'view-switcher__item--selected': !!matchComments() }}>
<A href={`/author/${props.authorSlug}/comments`}>{t('Comments')}</A>
<Show when={author()?.stat}>
<span class="view-switcher__counter">{author()?.stat?.comments || 0}</span>
</Show>
</li>
<li classList={{ 'view-switcher__item--selected': !!matchAbout() }}>
<A onClick={() => checkBioHeight()} href={`/author/${props.authorSlug}`}>
{t('About')}
</A>
</li>
</ul>
</div>
<div class={clsx('col-md-8', styles.additionalControls)}>
<Show when={author()?.stat?.rating || author()?.stat?.rating === 0}>
<div class={styles.ratingContainer}>
{t('All posts rating')}
<AuthorShoutsRating author={author() as Author} class={styles.ratingControl} />
</div>
</Show>
</div>
</div>
</>
</Show>
</div>
<Switch>
<Match when={matchAbout()}>
<div class="wide-container">
<div class="row">
<div class="col-md-20 col-lg-18">
<div
ref={(el) => (bioWrapperRef = el)}
class={styles.longBio}
classList={{ [styles.longBioExpanded]: isBioExpanded() }}
>
<div ref={(el) => (bioContainerRef = el)} innerHTML={author()?.about || ''} />
</div>
<Show when={showExpandBioControl()}>
<button
class={clsx('button button--subscribe-topic', styles.longBioExpandedControl)}
onClick={() => setIsBioExpanded(!isBioExpanded())}
>
{t('Show more')}
</button>
</Show>
</div>
</div>
</div>
</Match>
<Match when={matchComments()}>
<Show when={me()?.slug === props.authorSlug && !me().stat?.comments}>
<div class="wide-container">
<Placeholder type={loc?.pathname} mode="profile" />
</div>
</Show>
<div class="wide-container">
<div class="row">
<div class="col-md-20 col-lg-18">
<ul class={stylesArticle.comments}>
<For each={commented()?.sort(byCreated).reverse()}>
{(comment) => (
<Comment
comment={comment}
class={styles.comment}
showArticleLink={true}
onDelete={(id) => handleDeleteComment(id)}
/>
)}
</For>
</ul>
</div>
</div>
</div>
</Match>
<Match when={matchAuthor()}>
<Show when={me()?.slug === props.authorSlug && !me().stat?.shouts}>
<div class="wide-container">
<Placeholder type={loc?.pathname} mode="profile" />
</div>
</Show>
<Show when={sortedFeed().length > 0}>
<Row1 article={sortedFeed()[0]} noauthor={true} nodate={true} />
<Show when={sortedFeed().length > 1}>
<Switch>
<Match when={sortedFeed().length === 2}>
<Row2 articles={sortedFeed()} isEqual={true} noauthor={true} nodate={true} />
</Match>
<Match when={sortedFeed().length === 3}>
<Row3 articles={sortedFeed()} noauthor={true} nodate={true} />
</Match>
<Match when={sortedFeed().length > 3}>
<For each={pages()}>
{(page) => (
<>
<Row1 article={page[0]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(1, 3)} isEqual={true} noauthor={true} />
<Row1 article={page[3]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(4, 6)} isEqual={true} noauthor={true} />
<Row1 article={page[6]} noauthor={true} nodate={true} />
<Row2 articles={page.slice(7, 9)} isEqual={true} noauthor={true} />
</>
)}
</For>
</Match>
</Switch>
</Show>
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show>
</Show>
</Match>
</Switch>
</div>
)
}