author-feed+comments-paginate
This commit is contained in:
parent
82904bd1da
commit
8fbc85615c
64
package.json
64
package.json
|
@ -32,40 +32,40 @@
|
|||
"@solid-primitives/pagination": "^0.3.0",
|
||||
"@solid-primitives/script-loader": "^2.2.0",
|
||||
"@solid-primitives/share": "^2.0.6",
|
||||
"@solid-primitives/storage": "^3.7.1",
|
||||
"@solid-primitives/storage": "^3.8.0",
|
||||
"@solid-primitives/upload": "^0.0.117",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.13.6",
|
||||
"@solidjs/start": "^1.0.4",
|
||||
"@tiptap/core": "^2.4.0",
|
||||
"@tiptap/extension-blockquote": "^2.4.0",
|
||||
"@tiptap/extension-bold": "^2.4.0",
|
||||
"@tiptap/extension-bubble-menu": "^2.4.0",
|
||||
"@tiptap/extension-bullet-list": "^2.4.0",
|
||||
"@tiptap/extension-character-count": "^2.4.0",
|
||||
"@tiptap/extension-collaboration": "^2.4.0",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.4.0",
|
||||
"@tiptap/extension-document": "^2.4.0",
|
||||
"@tiptap/extension-dropcursor": "^2.4.0",
|
||||
"@tiptap/extension-floating-menu": "^2.4.0",
|
||||
"@tiptap/extension-focus": "^2.4.0",
|
||||
"@tiptap/extension-gapcursor": "^2.4.0",
|
||||
"@tiptap/extension-hard-break": "^2.4.0",
|
||||
"@tiptap/extension-heading": "^2.4.0",
|
||||
"@tiptap/extension-highlight": "^2.4.0",
|
||||
"@tiptap/extension-history": "^2.4.0",
|
||||
"@tiptap/extension-horizontal-rule": "^2.4.0",
|
||||
"@tiptap/extension-image": "^2.4.0",
|
||||
"@tiptap/extension-italic": "^2.4.0",
|
||||
"@tiptap/extension-link": "^2.4.0",
|
||||
"@tiptap/extension-list-item": "^2.4.0",
|
||||
"@tiptap/extension-ordered-list": "^2.4.0",
|
||||
"@tiptap/extension-paragraph": "^2.4.0",
|
||||
"@tiptap/extension-placeholder": "^2.4.0",
|
||||
"@tiptap/extension-strike": "^2.4.0",
|
||||
"@tiptap/extension-text": "^2.4.0",
|
||||
"@tiptap/extension-underline": "^2.4.0",
|
||||
"@tiptap/extension-youtube": "^2.4.0",
|
||||
"@tiptap/core": "^2.5.1",
|
||||
"@tiptap/extension-blockquote": "^2.5.1",
|
||||
"@tiptap/extension-bold": "^2.5.1",
|
||||
"@tiptap/extension-bubble-menu": "^2.5.1",
|
||||
"@tiptap/extension-bullet-list": "^2.5.1",
|
||||
"@tiptap/extension-character-count": "^2.5.1",
|
||||
"@tiptap/extension-collaboration": "^2.5.1",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.5.1",
|
||||
"@tiptap/extension-document": "^2.5.1",
|
||||
"@tiptap/extension-dropcursor": "^2.5.1",
|
||||
"@tiptap/extension-floating-menu": "^2.5.1",
|
||||
"@tiptap/extension-focus": "^2.5.1",
|
||||
"@tiptap/extension-gapcursor": "^2.5.1",
|
||||
"@tiptap/extension-hard-break": "^2.5.1",
|
||||
"@tiptap/extension-heading": "^2.5.1",
|
||||
"@tiptap/extension-highlight": "^2.5.1",
|
||||
"@tiptap/extension-history": "^2.5.1",
|
||||
"@tiptap/extension-horizontal-rule": "^2.5.1",
|
||||
"@tiptap/extension-image": "^2.5.1",
|
||||
"@tiptap/extension-italic": "^2.5.1",
|
||||
"@tiptap/extension-link": "^2.5.1",
|
||||
"@tiptap/extension-list-item": "^2.5.1",
|
||||
"@tiptap/extension-ordered-list": "^2.5.1",
|
||||
"@tiptap/extension-paragraph": "^2.5.1",
|
||||
"@tiptap/extension-placeholder": "^2.5.1",
|
||||
"@tiptap/extension-strike": "^2.5.1",
|
||||
"@tiptap/extension-text": "^2.5.1",
|
||||
"@tiptap/extension-underline": "^2.5.1",
|
||||
"@tiptap/extension-youtube": "^2.5.1",
|
||||
"@types/cookie": "^0.6.0",
|
||||
"@types/cookie-signature": "^1.1.2",
|
||||
"@types/node": "^20.14.10",
|
||||
|
@ -79,7 +79,7 @@
|
|||
"extended-eventsource": "^1.4.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.11.5",
|
||||
"i18next": "^23.12.1",
|
||||
"i18next-http-backend": "^2.5.2",
|
||||
"i18next-icu": "^2.3.0",
|
||||
"intl-messageformat": "^10.5.14",
|
||||
|
@ -99,7 +99,7 @@
|
|||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"stylelint-order": "^6.0.4",
|
||||
"stylelint-scss": "^6.4.1",
|
||||
"swiper": "^11.1.4",
|
||||
"swiper": "^11.1.5",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
"tslib": "^2.6.3",
|
||||
"typescript": "^5.5.3",
|
||||
|
|
|
@ -8,14 +8,11 @@ import { useFollowing } from '~/context/following'
|
|||
import { useGraphQL } from '~/context/graphql'
|
||||
import { useLocalize } from '~/context/localize'
|
||||
import { useSession } from '~/context/session'
|
||||
import { loadReactions } from '~/graphql/api/public'
|
||||
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 type { Author, Reaction, Shout, Topic } from '~/graphql/schema/core.gen'
|
||||
import { byCreated } from '~/lib/sort'
|
||||
import { paginate } from '~/utils/paginate'
|
||||
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
|
||||
import stylesArticle from '../../Article/Article.module.scss'
|
||||
import { Comment } from '../../Article/Comment'
|
||||
import { AuthorCard } from '../../Author/AuthorCard'
|
||||
|
@ -48,7 +45,6 @@ export const AuthorView = (props: AuthorViewProps) => {
|
|||
const { followers: myFollowers, follows: myFollows } = useFollowing()
|
||||
|
||||
// signals
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||
const [author, setAuthor] = createSignal<Author>()
|
||||
const [followers, setFollowers] = createSignal<Author[]>([] as Author[])
|
||||
|
@ -62,20 +58,6 @@ export const AuthorView = (props: AuthorViewProps) => {
|
|||
paginate(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
|
||||
)
|
||||
|
||||
// fx
|
||||
// пагинация загрузки ленты постов
|
||||
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(
|
||||
|
@ -122,24 +104,6 @@ export const AuthorView = (props: AuthorViewProps) => {
|
|||
)
|
||||
)
|
||||
|
||||
// 3 // догружает ленту и комментарии
|
||||
createEffect(
|
||||
on(
|
||||
() => author() as Author,
|
||||
async (profile: Author) => {
|
||||
if (!commented() && profile) {
|
||||
await loadMore()
|
||||
const commentsFetcher = loadReactions({
|
||||
by: { comment: true, created_by: profile.id }
|
||||
})
|
||||
const ccc = await commentsFetcher()
|
||||
if (ccc) setCommented((_) => ccc || [])
|
||||
}
|
||||
}
|
||||
// { defer: true },
|
||||
)
|
||||
)
|
||||
|
||||
// event handlers
|
||||
let bioContainerRef: HTMLDivElement
|
||||
let bioWrapperRef: HTMLDivElement
|
||||
|
@ -290,14 +254,6 @@ export const AuthorView = (props: AuthorViewProps) => {
|
|||
</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>
|
||||
|
|
|
@ -9,6 +9,7 @@ export type LoadMoreItems = Shout[] | Author[] | Reaction[]
|
|||
type LoadMoreProps = {
|
||||
loadFunction: (offset?: number) => Promise<LoadMoreItems>
|
||||
pageSize: number
|
||||
hidden?: boolean
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
|
@ -37,7 +38,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
|
|||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Show when={isLoadMoreButtonVisible()}>
|
||||
<Show when={isLoadMoreButtonVisible() && !props.hidden}>
|
||||
<div class="load-more-container">
|
||||
<Button
|
||||
onClick={loadItems}
|
||||
|
|
|
@ -24,6 +24,7 @@ type ReactionsContextType = {
|
|||
createReaction: (reaction: MutationCreate_ReactionArgs) => Promise<void>
|
||||
updateReaction: (reaction: MutationUpdate_ReactionArgs) => Promise<Reaction>
|
||||
deleteReaction: (id: number) => Promise<{ error: string } | null>
|
||||
addReactions: (rrr: Reaction[]) => void
|
||||
}
|
||||
|
||||
const ReactionsContext = createContext<ReactionsContextType>({} as ReactionsContextType)
|
||||
|
@ -38,24 +39,27 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
|||
const { t } = useLocalize()
|
||||
const { showSnackbar } = useSnackbar()
|
||||
const { mutation } = useGraphQL()
|
||||
|
||||
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
|
||||
!opts.by && console.warn('reactions provider got wrong opts')
|
||||
const fetcher = await loadReactions(opts)
|
||||
const result = (await fetcher()) || []
|
||||
console.debug('[context.reactions] loaded', result)
|
||||
const newReactionsByShout: Record<string, Reaction[]> = {}
|
||||
const newReactionEntities = result.reduce(
|
||||
const addReactions = (rrr: Reaction[]) => {
|
||||
const newReactionsByShout: Record<string, Reaction[]> = { ...reactionsByShout }
|
||||
const newReactionEntities = rrr.reduce(
|
||||
(acc: { [reaction_id: number]: Reaction }, reaction: Reaction) => {
|
||||
acc[reaction.id] = reaction
|
||||
if (!newReactionsByShout[reaction.shout.slug]) newReactionsByShout[reaction.shout.slug] = []
|
||||
newReactionsByShout[reaction.shout.slug].push(reaction)
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
{ ...reactionEntities }
|
||||
)
|
||||
setReactionsByShout(newReactionsByShout)
|
||||
setReactionEntities(newReactionEntities)
|
||||
setReactionsByShout(newReactionsByShout)
|
||||
}
|
||||
|
||||
const loadReactionsBy = async (opts: QueryLoad_Reactions_ByArgs): Promise<Reaction[]> => {
|
||||
!opts.by && console.warn('reactions provider got wrong opts')
|
||||
const fetcher = await loadReactions(opts)
|
||||
const result = (await fetcher()) || []
|
||||
console.debug('[context.reactions] loaded', result)
|
||||
result && addReactions(result)
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -120,7 +124,8 @@ export const ReactionsProvider = (props: { children: JSX.Element }) => {
|
|||
loadReactionsBy,
|
||||
createReaction,
|
||||
updateReaction,
|
||||
deleteReaction
|
||||
deleteReaction,
|
||||
addReactions
|
||||
}
|
||||
|
||||
const value: ReactionsContextType = { reactionEntities, reactionsByShout, ...actions }
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { RouteSectionProps, createAsync } from '@solidjs/router'
|
||||
import { ErrorBoundary, Suspense, createEffect, createMemo } from 'solid-js'
|
||||
import { ErrorBoundary, createEffect, createMemo } from 'solid-js'
|
||||
import { AuthorView } from '~/components/Views/Author'
|
||||
import { FourOuFourView } from '~/components/Views/FourOuFour'
|
||||
import { Loading } from '~/components/_shared/Loading'
|
||||
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
|
||||
import { PageLayout } from '~/components/_shared/PageLayout'
|
||||
import { useAuthors } from '~/context/authors'
|
||||
import { useFeed } from '~/context/feed'
|
||||
import { useLocalize } from '~/context/localize'
|
||||
import { ReactionsProvider } from '~/context/reactions'
|
||||
import { loadAuthors, loadShouts, loadTopics } from '~/graphql/api/public'
|
||||
import { ReactionsProvider, useReactions } from '~/context/reactions'
|
||||
import { loadAuthors, loadReactions, loadShouts, loadTopics } from '~/graphql/api/public'
|
||||
import {
|
||||
Author,
|
||||
LoadShoutsOptions,
|
||||
|
@ -38,10 +39,9 @@ const fetchAuthor = async (slug: string) => {
|
|||
export const route = {
|
||||
load: async ({ params, location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => {
|
||||
const offset: number = Number.parseInt(query.offset, 10)
|
||||
const result = await fetchAuthorShouts(params.slug, offset)
|
||||
return {
|
||||
author: await fetchAuthor(params.slug),
|
||||
shouts: result || [],
|
||||
shouts: await fetchAuthorShouts(params.slug, offset),
|
||||
topics: await fetchAllTopics()
|
||||
}
|
||||
}
|
||||
|
@ -50,11 +50,14 @@ export const route = {
|
|||
export type AuthorPageProps = { articles?: Shout[]; author?: Author; topics?: Topic[] }
|
||||
|
||||
export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
|
||||
const { addAuthor } = useAuthors()
|
||||
const { addAuthor, authorsEntities } = useAuthors()
|
||||
const articles = createAsync(
|
||||
async () => props.data.articles || (await fetchAuthorShouts(props.params.slug)) || []
|
||||
)
|
||||
const author = createAsync(async () => {
|
||||
const loadedBefore = authorsEntities()[props.params.slug]
|
||||
if (loadedBefore) return loadedBefore
|
||||
|
||||
const a = props.data.author || (await fetchAuthor(props.params.slug))
|
||||
a && addAuthor(a)
|
||||
return a
|
||||
|
@ -80,12 +83,32 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
|
|||
: getImageUrl('production/image/logo_image.png')
|
||||
)
|
||||
|
||||
const selectedTab = createMemo(() =>
|
||||
props.params.tab in ['followers', 'shouts'] ? props.params.tab : 'name'
|
||||
)
|
||||
const selectedTab = createMemo(() => (props.params.tab in ['comments', 'about'] ? props.params.tab : ''))
|
||||
const { addReactions } = useReactions()
|
||||
const loadMoreComments = async () => {
|
||||
const commentsFetcher = loadReactions({
|
||||
by: { comment: true, created_by: author()?.id }
|
||||
})
|
||||
const ccc = await commentsFetcher()
|
||||
ccc && addReactions(ccc)
|
||||
return ccc as LoadMoreItems
|
||||
}
|
||||
const { addFeed, feedByAuthor } = useFeed()
|
||||
const loadMoreAuthorShouts = async () => {
|
||||
const slug = author()?.slug
|
||||
const offset = feedByAuthor()[props.params.slug].length
|
||||
const shoutsFetcher = loadShouts({
|
||||
filters: { author: slug },
|
||||
offset,
|
||||
limit: SHOUTS_PER_PAGE
|
||||
})
|
||||
const sss = await shoutsFetcher()
|
||||
sss && addFeed(sss)
|
||||
return sss as LoadMoreItems
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={(_err) => <FourOuFourView />}>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<PageLayout
|
||||
title={`${t('Discours')} :: ${title()}`}
|
||||
headerTitle={author()?.name || ''}
|
||||
|
@ -94,16 +117,21 @@ export default function AuthorPage(props: RouteSectionProps<AuthorPageProps>) {
|
|||
cover={cover()}
|
||||
>
|
||||
<ReactionsProvider>
|
||||
<LoadMoreWrapper
|
||||
loadFunction={(selectedTab() === 'comments' ? loadMoreComments : loadMoreAuthorShouts)}
|
||||
pageSize={SHOUTS_PER_PAGE}
|
||||
hidden={selectedTab() !== '' || selectedTab() !== 'comments'}
|
||||
>
|
||||
<AuthorView
|
||||
author={author() as Author}
|
||||
selectedTab={selectedTab()}
|
||||
authorSlug={props.params.slug}
|
||||
shouts={articles() as Shout[]}
|
||||
shouts={feedByAuthor()[props.params.slug] || articles() as Shout[]}
|
||||
topics={topics()}
|
||||
/>
|
||||
</LoadMoreWrapper>
|
||||
</ReactionsProvider>
|
||||
</PageLayout>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
/**
|
||||
* Делит массив на меньшие массивы (страницы) заданного размера.
|
||||
*
|
||||
* @template T - Тип элементов в массиве.
|
||||
* @param {T[]} arr - Массив, который нужно разделить на страницы.
|
||||
* @param {number} startIndex - Индекс, с которого начинается пагинация.
|
||||
* @param {number} pageSize - Размер каждой страницы.
|
||||
* @returns {T[][]} - Массив массивов, где каждый подмассив является страницей заданного размера.
|
||||
*/
|
||||
export function paginate<T>(arr: T[], startIndex: number, pageSize: number): T[][] {
|
||||
return arr.slice(startIndex).reduce((acc, item, index) => {
|
||||
// Начинаем новую страницу, когда индекс кратен размеру страницы
|
||||
if (index % pageSize === 0) {
|
||||
acc.push([])
|
||||
// Создаем новый подмассив (страницу)
|
||||
acc.push([]);
|
||||
}
|
||||
|
||||
acc?.at(-1)?.push(item)
|
||||
return acc
|
||||
}, [] as T[][])
|
||||
// Добавляем текущий элемент на последнюю страницу
|
||||
acc.at(-1)?.push(item);
|
||||
return acc;
|
||||
}, [] as T[][]); // Инициализируем аккумулятор как пустой массив массивов
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user