load-more-main-ok

This commit is contained in:
Untone 2024-07-15 23:35:33 +03:00
parent f6f012449d
commit 4fe2768329
9 changed files with 218 additions and 102 deletions

View File

@ -51,7 +51,7 @@ const data: PlaceholderData = {
text: 'Placeholder feedDiscussions',
buttonLabelAuthor: 'Current discussions',
buttonLabelFeed: 'Enter',
href: '/feed?by=last_comment'
href: '/feed/hot'
},
author: {
image: 'placeholder-join.webp',
@ -71,7 +71,7 @@ const data: PlaceholderData = {
header: 'Join discussions',
text: 'Placeholder feedDiscussions',
buttonLabel: 'Go to discussions',
href: '/feed?by=last_comment',
href: '/feed/hot',
profileLinks: [
{
href: '/debate',

View File

@ -500,14 +500,14 @@ export const Header = (props: Props) => {
</span>
</A>
</li>
<li>
{/* <li>
<A href={'/feed/bookmarked'}>
<span class={styles.subnavigationItemName}>
<Icon name="bookmark" class={styles.icon} />
{t('Bookmarks')}
</span>
</A>
</li>
</li> */}
</ul>
</div>
</nav>

View File

@ -1,6 +1,6 @@
import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router'
import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { DropDown } from '~/components/_shared/DropDown'
import { Option } from '~/components/_shared/DropDown/DropDown'
import { Icon } from '~/components/_shared/Icon'
@ -18,7 +18,7 @@ import { useUI } from '~/context/ui'
import { loadUnratedShouts } from '~/graphql/api/private'
import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { byCreated } from '~/lib/sort'
import { FeedSearchParams } from '~/routes/feed/(feed)'
import { FeedSearchParams } from '~/routes/feed/[...order]'
import { CommentDate } from '../../Article/CommentDate'
import { getShareUrl } from '../../Article/SharePopup'
import { AuthorBadge } from '../../Author/AuthorBadge'
@ -36,6 +36,7 @@ export type PeriodType = 'week' | 'month' | 'year'
export type FeedProps = {
shouts?: Shout[]
mode?: '' | 'likes' | 'hot'
}
export const FeedView = (props: FeedProps) => {
@ -53,7 +54,7 @@ export const FeedView = (props: FeedProps) => {
const [isLoading, setIsLoading] = createSignal(false)
const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false)
const { session } = useSession()
const { nonfeaturedFeed, setNonFeaturedFeed } = useFeed()
const { feed, setFeed } = useFeed()
const { loadReactionsBy } = useReactions()
const { topTopics } = useTopics()
const { topAuthors } = useAuthors()
@ -67,20 +68,13 @@ export const FeedView = (props: FeedProps) => {
setTopComments(comments.sort(byCreated).reverse())
}
onMount(
() =>
props.shouts &&
Array.isArray(props.shouts) &&
setNonFeaturedFeed((prev) => [...prev, ...(props.shouts || [])]) && console.info(nonfeaturedFeed())
)
createEffect(
on(
() => nonfeaturedFeed(),
feed,
(sss?: Shout[]) => {
if (sss && Array.isArray(sss)) {
setIsLoading(true)
setNonFeaturedFeed((prev) => [...prev, ...sss])
setFeed((prev) => [...prev, ...sss])
Promise.all([
loadTopComments(),
loadReactionsBy({ by: { shouts: sss.map((s: Shout) => s.slug) } })
@ -113,40 +107,33 @@ export const FeedView = (props: FeedProps) => {
<Placeholder type={loc?.pathname} mode="feed" />
</Show>
<Show when={(session() || loc?.pathname === 'feed') && nonfeaturedFeed()?.length}>
<Show when={(session() || loc?.pathname === 'feed') && feed()?.length}>
<div class={styles.filtersContainer}>
<ul class={clsx('view-switcher', styles.feedFilter)}>
<li
class={clsx({
'view-switcher__item--selected': searchParams?.by === 'after' || !searchParams?.by
})}
>
<li class={clsx({ 'view-switcher__item--selected': !props.mode })}>
<A href={loc.pathname}>{t('Recent')}</A>
</li>
{/*<li>*/}
{/* <a href="/feed/?by=views">{t('Most read')}</a>*/}
{/*</li>*/}
<li
class={clsx({
'view-switcher__item--selected': searchParams?.by === 'likes'
'view-switcher__item--selected': props.mode === 'likes'
})}
>
<span class="link" onClick={() => changeSearchParams({ by: 'likes' })}>
{t('Top rated')}
</span>
<A class="link" href={'/feed/likes'}>
{t('Liked')}
</A>
</li>
<li
class={clsx({
'view-switcher__item--selected': searchParams?.by === 'last_comment'
'view-switcher__item--selected': props.mode === 'hot'
})}
>
<span class="link" onClick={() => changeSearchParams({ by: 'last_comment' })}>
<A class="link" href={'/feed/hot'}>
{t('Commented')}
</span>
</A>
</li>
</ul>
<div class={styles.dropdowns}>
<Show when={searchParams?.by && searchParams?.by !== 'after'}>
<Show when={searchParams?.period}>
<DropDown
popupProps={{ horizontalAnchor: 'right' }}
options={asOptions(['week', 'month', 'year'])}
@ -157,7 +144,7 @@ export const FeedView = (props: FeedProps) => {
</Show>
<DropDown
popupProps={{ horizontalAnchor: 'right' }}
options={asOptions(['followed', 'unrated', 'discussed', 'bookmarked', 'coauthored'])}
options={asOptions(['followed', 'unrated', 'discussed', 'coauthored'])}
currentOption={asOption(loc.pathname.split('/').pop() || '')}
triggerCssClass={styles.periodSwitcher}
onChange={(mode: Option) => navigate(`/feed/${mode.value}`)}
@ -166,8 +153,8 @@ export const FeedView = (props: FeedProps) => {
</div>
<Show when={!isLoading()} fallback={<Loading />}>
<Show when={(nonfeaturedFeed() || []).length > 0}>
<For each={(nonfeaturedFeed() || []).slice(0, 4)}>
<Show when={(feed() || []).length > 0}>
<For each={(feed() || []).slice(0, 4)}>
{(article) => (
<ArticleCard
onShare={(shared) => handleShare(shared)}
@ -199,7 +186,7 @@ export const FeedView = (props: FeedProps) => {
</ul>
</div>
<For each={(nonfeaturedFeed() || []).slice(4)}>
<For each={(feed() || []).slice(4)}>
{(article) => (
<ArticleCard article={article} settings={{ isFeedMode: true }} desktopCoverSize="M" />
)}

View File

@ -4,17 +4,17 @@ import { useLocalize } from '~/context/localize'
import { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
export type LoadMoreItems = Shout[] | Author[] | Reaction[]
type LoadMoreProps = {
loadFunction: (offset?: number) => void
loadFunction: (offset?: number) => Promise<LoadMoreItems>
pageSize: number
children: JSX.Element
}
type Items = Shout[] | Author[] | Reaction[]
export const LoadMoreWrapper = (props: LoadMoreProps) => {
const { t } = useLocalize()
const [items, setItems] = createSignal<Items>([])
const [items, setItems] = createSignal<LoadMoreItems>([])
const [offset, setOffset] = createSignal(0)
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(true)
const [isLoading, setIsLoading] = createSignal(false)
@ -25,7 +25,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => {
const newItems = await props.loadFunction(offset())
if (!Array.isArray(newItems)) return
console.debug('[_share] load more items', newItems)
setItems((prev) => [...prev, ...newItems])
setItems((prev) => [...prev, ...newItems] as LoadMoreItems)
setOffset((prev) => prev + props.pageSize)
setIsLoadMoreButtonVisible(newItems.length >= props.pageSize - 1)
setIsLoading(false)

View File

@ -34,9 +34,9 @@ type FeedContextType = {
seen: Accessor<{ [slug: string]: number }>
addSeen: (slug: string) => void
// featured
nonfeaturedFeed: Accessor<Shout[] | undefined>
setNonFeaturedFeed: Setter<Shout[]>
// all
feed: Accessor<Shout[] | undefined>
setFeed: Setter<Shout[]>
// featured
featuredFeed: Accessor<Shout[] | undefined>
@ -62,7 +62,7 @@ export const useFeed = () => useContext(FeedContext)
export const FeedProvider = (props: { children: JSX.Element }) => {
const [sortedFeed, setSortedFeed] = createSignal<Shout[]>([])
const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
const [nonfeaturedFeed, setNonFeaturedFeed] = createSignal<Shout[]>([])
const [feed, setFeed] = createSignal<Shout[]>([])
const [featuredFeed, setFeaturedFeed] = createSignal<Shout[]>([])
const [expoFeed, setExpoFeed] = createSignal<Shout[]>([])
const [topFeed, setTopFeed] = createSignal<Shout[]>([])
@ -260,8 +260,8 @@ export const FeedProvider = (props: { children: JSX.Element }) => {
setFeaturedFeed,
expoFeed,
setExpoFeed,
nonfeaturedFeed,
setNonFeaturedFeed
feed,
setFeed
}}
>
{props.children}

View File

@ -263,6 +263,7 @@
"Lists": "Списки",
"Literature": "Литература",
"Load more": "Показать ещё",
"loaded": "загружено",
"Loading": "Загрузка",
"Login and security": "Вход и безопасность",
"Logout": "Выход",

View File

@ -1,12 +1,10 @@
import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router'
import { Show, createEffect } from 'solid-js'
import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { useFeed } from '~/context/feed'
import { useTopics } from '~/context/topics'
import { loadShouts, loadTopics } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { byStat } from '~/lib/sort'
import { SortFunction } from '~/types/common'
import { HomeView, HomeViewProps } from '../components/Views/Home'
import { Loading } from '../components/_shared/Loading'
import { PageLayout } from '../components/_shared/PageLayout'
@ -74,7 +72,6 @@ export const route = {
} satisfies RouteDefinition
export default function HomePage(props: RouteSectionProps<HomeViewProps>) {
const { addTopics } = useTopics()
const { t } = useLocalize()
const {
setFeaturedFeed,
@ -85,46 +82,38 @@ export default function HomePage(props: RouteSectionProps<HomeViewProps>) {
topFeed: topRatedFeed
} = useFeed()
const data = createAsync(async (prev?: HomeViewProps) => {
const topics = props.data?.topics || (await fetchAllTopics())
const offset = prev?.featuredShouts?.length || 0
const featuredShoutsLoader = featuredLoader(offset)
const loaded = await featuredShoutsLoader()
setFeaturedFeed((prev) => [...prev, ...loaded||[]])
const featuredShouts = [
...(prev?.featuredShouts || []),
...(loaded || props.data?.featuredShouts || [])
]
const sortFn = byStat('viewed')
const topViewedShouts = featuredShouts.sort(sortFn as SortFunction<Shout>)
return {
...prev,
...props.data,
topViewedShouts,
featuredShouts,
topics
}
})
// preload all topics
const { addTopics, sortedTopics } = useTopics()
createEffect(() => {
if (data()?.topics) {
console.debug('[routes.main] topics update')
addTopics(data()?.topics || [])
}
!sortedTopics() && props.data.topics && addTopics(props.data.topics)
})
// load more faetured shouts
const loadMoreFeatured = async (offset?: number) => {
const shoutsLoader = featuredLoader(offset)
const loaded = await shoutsLoader()
loaded && setFeaturedFeed((prev: Shout[]) => [...prev, ...loaded])
return loaded as LoadMoreItems
}
// preload featured shouts
const shouts = createAsync(async () => {
if (props.data.featuredShouts) {
setFeaturedFeed(props.data.featuredShouts)
console.debug('[routes.main] featured feed preloaded')
return props.data.featuredShouts
}
return await loadMoreFeatured()
})
const SHOUTS_PER_PAGE = 20
return (
<PageLayout withPadding={true} title={t('Discours')} key="home">
<Show when={(featuredFeed() || []).length > 0} fallback={<Loading />}>
<LoadMoreWrapper loadFunction={loadMoreFeatured} pageSize={SHOUTS_PER_PAGE}>
<HomeView
featuredShouts={featuredFeed() as Shout[]}
featuredShouts={featuredFeed() || shouts() as Shout[]}
topMonthShouts={topMonthFeed() as Shout[]}
topViewedShouts={topViewedFeed() as Shout[]}
topRatedShouts={topRatedFeed() as Shout[]}

View File

@ -1,15 +1,16 @@
import { RouteSectionProps, createAsync, useSearchParams } from '@solidjs/router'
import { Client } from '@urql/core'
import { createSignal } from 'solid-js'
import { createEffect } from 'solid-js'
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
import { Feed } from '~/components/Views/Feed'
import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { PageLayout } from '~/components/_shared/PageLayout'
import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize'
import { ReactionsProvider } from '~/context/reactions'
import { useTopics } from '~/context/topics'
import { loadShouts } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
import { SHOUTS_PER_PAGE } from '../(main)'
export type FeedPeriod = 'week' | 'month' | 'year'
@ -20,7 +21,6 @@ export type PeriodItem = {
}
export type FeedSearchParams = {
by: 'after' | 'likes' | 'last_comment'
period: FeedPeriod
}
@ -44,42 +44,67 @@ const getFromDate = (period: FeedPeriod): number => {
return Math.floor(d.getTime() / 1000)
}
const fetchPublishedShouts = async (offset?: number, _client?: Client) => {
const shoutsLoader = loadShouts({ filters: { featured: undefined }, limit: SHOUTS_PER_PAGE, offset })
const feedLoader = async (options: Partial<LoadShoutsOptions>, _client?: Client) => {
const shoutsLoader = loadShouts({ ...options, limit: SHOUTS_PER_PAGE } as LoadShoutsOptions)
return await shoutsLoader()
}
export const route = {
load: async ({ location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => {
const offset: number = Number.parseInt(query.offset, 10)
const result = await fetchPublishedShouts(offset)
const result = await feedLoader({ offset })
return result
}
}
export default (props: RouteSectionProps<Shout[]>) => {
const [searchParams] = useSearchParams<FeedSearchParams>()
export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => {
const [searchParams] = useSearchParams<FeedSearchParams>() // ?period=month
const { t } = useLocalize()
const {setNonFeaturedFeed} = useFeed()
const [offset, setOffset] = createSignal<number>(0)
const loadMore = async () => {
const newOffset = offset() + SHOUTS_PER_PAGE
setOffset(newOffset)
const { setFeed } = useFeed()
// preload all topics
const { addTopics, sortedTopics } = useTopics()
createEffect(() => {
!sortedTopics() && props.data.topics && addTopics(props.data.topics)
})
// load more feed
const loadMoreFeed = async (offset?: number) => {
// /feed/:order: - select order setting
const paramPattern = /^(hot|likes)$/
const order =
(props.params.order && paramPattern.test(props.params.order)
? props.params.order === 'hot'
? 'last_comment'
: props.params.order
: 'created_at') || 'created_at'
const options: LoadShoutsOptions = {
limit: SHOUTS_PER_PAGE,
offset: newOffset,
order_by: searchParams?.by
offset,
order_by: order
}
if (searchParams?.by === 'after') {
const period = searchParams?.by || 'month'
// ?period=month - time period filter
if (searchParams?.period) {
const period = searchParams?.period || 'month'
options.filters = { after: getFromDate(period as FeedPeriod) }
}
const result = await fetchPublishedShouts(newOffset)
result && setNonFeaturedFeed(result)
return
const loaded = await feedLoader(options)
loaded && setFeed((prev: Shout[]) => [...prev, ...loaded])
return loaded as LoadMoreItems
}
const shouts = createAsync(async () => props.data || await loadMore())
// preload shouts
const shouts = createAsync(async () => {
if (props.data.shouts) {
setFeed(props.data.shouts)
console.debug('[routes.main] feed preloaded')
return props.data.shouts
}
return (await loadMoreFeed()) as Shout[]
})
return (
<PageLayout
@ -88,9 +113,9 @@ export default (props: RouteSectionProps<Shout[]>) => {
key="feed"
desc="Independent media project about culture, science, art and society with horizontal editing"
>
<LoadMoreWrapper loadFunction={loadMore} pageSize={AUTHORS_PER_PAGE}>
<LoadMoreWrapper loadFunction={loadMoreFeed} pageSize={AUTHORS_PER_PAGE}>
<ReactionsProvider>
<Feed shouts={shouts() || []} />
<Feed />
</ReactionsProvider>
</LoadMoreWrapper>
</PageLayout>

View File

@ -0,0 +1,114 @@
import { RouteSectionProps, useSearchParams } from '@solidjs/router'
import { createEffect } from 'solid-js'
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
import { Feed } from '~/components/Views/Feed'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { PageLayout } from '~/components/_shared/PageLayout'
import { useFeed } from '~/context/feed'
import { useGraphQL } from '~/context/graphql'
import { useLocalize } from '~/context/localize'
import { ReactionsProvider } from '~/context/reactions'
import { useTopics } from '~/context/topics'
import {
loadCoauthoredShouts,
loadDiscussedShouts,
loadFollowedShouts,
loadUnratedShouts
} from '~/graphql/api/private'
import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen'
const feeds = {
followed: loadFollowedShouts,
discussed: loadDiscussedShouts,
coauthored: loadCoauthoredShouts,
unrated: loadUnratedShouts
}
export type FeedPeriod = 'week' | 'month' | 'year'
export type FeedSearchParams = { period?: FeedPeriod }
const getFromDate = (period: FeedPeriod): number => {
const now = new Date()
let d: Date = now
switch (period) {
case 'week': {
d = new Date(now.setDate(now.getDate() - 7))
break
}
case 'month': {
d = new Date(now.setMonth(now.getMonth() - 1))
break
}
case 'year': {
d = new Date(now.setFullYear(now.getFullYear() - 1))
break
}
}
return Math.floor(d.getTime() / 1000)
}
// /feed/my/followed/hot
export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => {
const [searchParams] = useSearchParams<FeedSearchParams>() // ?period=month
const { t } = useLocalize()
const { setFeed } = useFeed()
// TODO: use const { requireAuthentication } = useSession()
const client = useGraphQL()
// preload all topics
const { addTopics, sortedTopics } = useTopics()
createEffect(() => {
!sortedTopics() && props.data.topics && addTopics(props.data.topics)
})
// load more my feed
const loadMoreMyFeed = async (offset?: number) => {
// /feed/my/:mode:
const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/
const mode =
props.params.mode && paramModePattern.test(props.params.mode) ? props.params.mode : 'followed'
const gqlHandler = feeds[mode as keyof typeof feeds]
// /feed/my/:mode:/:order: - select order setting
const paramOrderPattern = /^(hot|likes)$/
const order =
(paramOrderPattern.test(props.params.order)
? props.params.order === 'hot'
? 'last_comment'
: props.params.order
: 'created_at') || 'created_at'
const options: LoadShoutsOptions = {
limit: 20,
offset,
order_by: order
}
// ?period=month - time period filter
if (searchParams?.period) {
const period = searchParams?.period || 'month'
options.filters = { after: getFromDate(period as FeedPeriod) }
}
const shoutsLoader = gqlHandler(client, options)
const loaded = await shoutsLoader()
loaded && setFeed((prev: Shout[]) => [...prev, ...loaded])
return loaded as LoadMoreItems
}
return (
<PageLayout
withPadding={true}
title={`${t('Discours')} :: ${t('Feed')}`}
key="feed"
desc="Independent media project about culture, science, art and society with horizontal editing"
>
<LoadMoreWrapper loadFunction={loadMoreMyFeed} pageSize={AUTHORS_PER_PAGE}>
<ReactionsProvider>
<Feed />
</ReactionsProvider>
</LoadMoreWrapper>
</PageLayout>
)
}