load-more-wrapper-wip

This commit is contained in:
Untone 2024-07-15 17:28:08 +03:00
parent 2b7a825bc5
commit 789a7497a3
9 changed files with 258 additions and 233 deletions

View File

@ -34,18 +34,14 @@ export default defineConfig({
}, },
vite: { vite: {
envPrefix: 'PUBLIC_', envPrefix: 'PUBLIC_',
plugins: [ plugins: [!isVercel && mkcert(), nodePolyfills(polyfillOptions), sassDts()],
!isVercel && mkcert(),
nodePolyfills(polyfillOptions),
sassDts()
],
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
additionalData: '@import "src/styles/imports";\n', additionalData: '@import "src/styles/imports";\n',
includePaths: ['./public', './src/styles'] includePaths: ['./public', './src/styles']
} }
} as CSSOptions["preprocessorOptions"] } as CSSOptions['preprocessorOptions']
} }
} }
} as SolidStartInlineConfig) } as SolidStartInlineConfig)

View File

@ -1,19 +1,15 @@
import type { Shout } from '~/graphql/schema/core.gen'
import { For, Show, createResource, createSignal, onCleanup } from 'solid-js' import { For, Show, createResource, createSignal, onCleanup } from 'solid-js'
import { debounce } from 'throttle-debounce' import { debounce } from 'throttle-debounce'
import { Button } from '~/components/_shared/Button' import { Button } from '~/components/_shared/Button'
import { Icon } from '~/components/_shared/Icon' import { Icon } from '~/components/_shared/Icon'
import { useFeed } from '~/context/feed' import { useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import type { Shout } from '~/graphql/schema/core.gen'
import { byScore } from '~/lib/sort' import { byScore } from '~/lib/sort'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed' import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed'
import { SearchResultItem } from './SearchResultItem'
import styles from './SearchModal.module.scss' import styles from './SearchModal.module.scss'
import { SearchResultItem } from './SearchResultItem'
// @@TODO handle empty article options after backend support (subtitle, cover, etc.) // @@TODO handle empty article options after backend support (subtitle, cover, etc.)
// @@TODO implement load more // @@TODO implement load more

View File

@ -1,25 +1,27 @@
import { clsx } from 'clsx'
import { For, Show, createEffect, createSignal, on, onCleanup, onMount } from 'solid-js'
import { A } from '@solidjs/router' import { A } from '@solidjs/router'
import { Button } from '~/components/_shared/Button' import { clsx } from 'clsx'
import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper' import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper'
import { useFeed } from '~/context/feed'
import { useGraphQL } from '~/context/graphql' import { useGraphQL } from '~/context/graphql'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import getShoutsQuery from '~/graphql/query/core/articles-load-by' import { loadShouts } from '~/graphql/api/public'
import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top'
import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LoadShoutsFilters, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { SHOUTS_PER_PAGE } from '~/routes/(main)'
import { LayoutType } from '~/types/common' import { LayoutType } from '~/types/common'
import { getUnixtime } from '~/utils/date' import { getUnixtime } from '~/utils/date'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { ArticleCard } from '../../Feed/ArticleCard' import { ArticleCard } from '../../Feed/ArticleCard'
import styles from './Expo.module.scss' import styles from './Expo.module.scss'
type Props = { type Props = {
shouts: Shout[] shouts: Shout[]
layout: LayoutType topMonthShouts?: Shout[]
topRatedShouts?: Shout[]
layout?: LayoutType
} }
export const PRERENDERED_ARTICLES_COUNT = 36 export const PRERENDERED_ARTICLES_COUNT = 36
@ -28,52 +30,28 @@ const LOAD_MORE_PAGE_SIZE = 12
export const Expo = (props: Props) => { export const Expo = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { query } = useGraphQL() const { query } = useGraphQL()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([]) const [favoriteTopArticles, setFavoriteTopArticles] = createSignal<Shout[]>([])
const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal<Shout[]>([])
const [articlesEndPage, setArticlesEndPage] = createSignal<number>(PRERENDERED_ARTICLES_COUNT)
const [expoShouts, setExpoShouts] = createSignal<Shout[]>([]) const [expoShouts, setExpoShouts] = createSignal<Shout[]>([])
const getLoadShoutsFilters = (additionalFilters: LoadShoutsFilters = {}): LoadShoutsFilters => { const { feedByLayout, expoFeed, setExpoFeed } = useFeed()
const filters = { ...additionalFilters } const layouts = createMemo<LayoutType[]>(() =>
props.layout ? [props.layout] : ['audio', 'video', 'image', 'literature']
)
if (!filters.layouts) filters.layouts = [] const loadMoreFiltered = async () => {
if (props.layout) { const limit = SHOUTS_PER_PAGE
filters.layouts.push(props.layout) const offset = (props.layout ? feedByLayout()[props.layout] : expoFeed())?.length
} else { const filters: LoadShoutsFilters = { layouts: layouts(), featured: true }
filters.layouts.push('audio', 'video', 'image', 'literature') const options: LoadShoutsOptions = { filters, limit, offset }
} const shoutsFetcher = loadShouts(options)
const result = await shoutsFetcher()
return filters result && setExpoFeed(result)
} return result
const loadMore = async (count: number) => {
const options: LoadShoutsOptions = {
filters: getLoadShoutsFilters(),
limit: count,
offset: expoShouts().length
}
options.filters = props.layout
? { layouts: [props.layout] }
: { layouts: ['audio', 'video', 'image', 'literature'] }
const resp = await query(getShoutsQuery, options).toPromise()
const result = resp?.data?.load_shouts || []
const hasMore = result.length !== options.limit + 1 && result.length !== 0
setIsLoadMoreButtonVisible(hasMore)
setExpoShouts((prev) => [...prev, ...result])
}
const loadMoreWithoutScrolling = async (count: number) => {
saveScrollPosition()
await loadMore(count)
restoreScrollPosition()
} }
const loadRandomTopArticles = async () => { const loadRandomTopArticles = async () => {
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
filters: { ...getLoadShoutsFilters(), featured: true }, filters: { layouts: layouts(), featured: true },
limit: 10, limit: 10,
random_limit: 100 random_limit: 100
} }
@ -84,19 +62,16 @@ export const Expo = (props: Props) => {
const loadRandomTopMonthArticles = async () => { const loadRandomTopMonthArticles = async () => {
const now = new Date() const now = new Date()
const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1))) const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1)))
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
filters: { ...getLoadShoutsFilters({ after }), reacted: true }, filters: { layouts: layouts(), after, reacted: true },
limit: 10, limit: 10,
random_limit: 10 random_limit: 10
} }
const resp = await query(getRandomTopShoutsQuery, { options }).toPromise() const resp = await query(getRandomTopShoutsQuery, { options }).toPromise()
setReactedTopMonthArticles(resp?.data?.load_shouts_random_top || []) setReactedTopMonthArticles(resp?.data?.load_shouts_random_top || [])
} }
onMount(() => { onMount(() => {
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
loadRandomTopArticles() loadRandomTopArticles()
loadRandomTopMonthArticles() loadRandomTopMonthArticles()
}) })
@ -106,11 +81,8 @@ export const Expo = (props: Props) => {
() => props.layout, () => props.layout,
() => { () => {
setExpoShouts([]) setExpoShouts([])
setIsLoadMoreButtonVisible(false)
setFavoriteTopArticles([]) setFavoriteTopArticles([])
setReactedTopMonthArticles([]) setReactedTopMonthArticles([])
setArticlesEndPage(PRERENDERED_ARTICLES_COUNT)
loadMore(PRERENDERED_ARTICLES_COUNT + LOAD_MORE_PAGE_SIZE)
loadRandomTopArticles() loadRandomTopArticles()
loadRandomTopMonthArticles() loadRandomTopMonthArticles()
} }
@ -120,14 +92,7 @@ export const Expo = (props: Props) => {
onCleanup(() => { onCleanup(() => {
setExpoShouts([]) setExpoShouts([])
}) })
const ExpoTabs = () => (
const handleLoadMoreClick = () => {
loadMoreWithoutScrolling(LOAD_MORE_PAGE_SIZE)
setArticlesEndPage((prev) => prev + LOAD_MORE_PAGE_SIZE)
}
console.log(props.layout)
return (
<div class={styles.Expo}>
<div class="wide-container"> <div class="wide-container">
<ul class={clsx('view-switcher')}> <ul class={clsx('view-switcher')}>
<li class={clsx({ 'view-switcher__item--selected': !props.layout })}> <li class={clsx({ 'view-switcher__item--selected': !props.layout })}>
@ -143,9 +108,9 @@ export const Expo = (props: Props) => {
<span class={clsx('linkReplacement')}>{t('Literature')}</span> <span class={clsx('linkReplacement')}>{t('Literature')}</span>
</ConditionalWrapper> </ConditionalWrapper>
</li> </li>
<li class={clsx({ 'view-switcher__item--selected': props.layout === ('audio' as LayoutType) })}> <li class={clsx({ 'view-switcher__item--selected': props.layout === 'audio' })}>
<ConditionalWrapper <ConditionalWrapper
condition={props.layout !== ('audio' as LayoutType)} condition={props.layout !== 'audio'}
wrapper={(children) => <A href={'/expo/audio'}>{children}</A>} wrapper={(children) => <A href={'/expo/audio'}>{children}</A>}
> >
<span class={clsx('linkReplacement')}>{t('Music')}</span> <span class={clsx('linkReplacement')}>{t('Music')}</span>
@ -169,11 +134,11 @@ export const Expo = (props: Props) => {
</li> </li>
</ul> </ul>
</div> </div>
)
<Show when={expoShouts().length > 0} fallback={<Loading />}> const ExpoGrid = () => (
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<For each={expoShouts()?.slice(0, LOAD_MORE_PAGE_SIZE)}> <For each={props.shouts.slice(0, LOAD_MORE_PAGE_SIZE)}>
{(shout) => ( {(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3"> <div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard <ArticleCard
@ -188,7 +153,7 @@ export const Expo = (props: Props) => {
<Show when={reactedTopMonthArticles()?.length > 0} keyed={true}> <Show when={reactedTopMonthArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} /> <ArticleCardSwiper title={t('Top month')} slides={reactedTopMonthArticles()} />
</Show> </Show>
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}> <For each={(props.topMonthShouts || []).slice(LOAD_MORE_PAGE_SIZE, LOAD_MORE_PAGE_SIZE * 2)}>
{(shout) => ( {(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3"> <div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard <ArticleCard
@ -203,7 +168,7 @@ export const Expo = (props: Props) => {
<Show when={favoriteTopArticles()?.length > 0} keyed={true}> <Show when={favoriteTopArticles()?.length > 0} keyed={true}>
<ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} /> <ArticleCardSwiper title={t('Favorite')} slides={favoriteTopArticles()} />
</Show> </Show>
<For each={expoShouts().slice(LOAD_MORE_PAGE_SIZE * 2, articlesEndPage())}> <For each={props.topRatedShouts?.slice(LOAD_MORE_PAGE_SIZE * 2, expoShouts().length)}>
{(shout) => ( {(shout) => (
<div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3"> <div class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard <ArticleCard
@ -216,12 +181,17 @@ export const Expo = (props: Props) => {
)} )}
</For> </For>
</div> </div>
<Show when={isLoadMoreButtonVisible()}>
<div class={styles.showMore}>
<Button size="L" onClick={handleLoadMoreClick} value={t('Load more')} />
</div>
</Show>
</div> </div>
)
return (
<div class={styles.Expo}>
<ExpoTabs />
<Show when={expoShouts().length > 0} fallback={<Loading />}>
<LoadMoreWrapper loadFunction={loadMoreFiltered} pageSize={LOAD_MORE_PAGE_SIZE}>
<ExpoGrid />
</LoadMoreWrapper>
</Show> </Show>
</div> </div>
) )

View File

@ -6,6 +6,7 @@ import styles from './Button.module.scss'
export type ButtonVariant = 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline' | 'danger' export type ButtonVariant = 'primary' | 'secondary' | 'bordered' | 'inline' | 'light' | 'outline' | 'danger'
type Props = { type Props = {
title?: string
value: string | JSX.Element value: string | JSX.Element
size?: 'S' | 'M' | 'L' size?: 'S' | 'M' | 'L'
variant?: ButtonVariant variant?: ButtonVariant
@ -28,6 +29,7 @@ export const Button = (props: Props) => {
} }
props.ref = el props.ref = el
}} }}
title={props.title || (typeof props.value === 'string' ? props.value : '')}
onClick={props.onClick} onClick={props.onClick}
type={props.type ?? 'button'} type={props.type ?? 'button'}
disabled={props.loading || props.disabled} disabled={props.loading || props.disabled}

View File

@ -0,0 +1,51 @@
import { JSX, Show, createSignal, onMount } from 'solid-js'
import { Button } from '~/components/_shared/Button'
import { useLocalize } from '~/context/localize'
import { Author, Reaction, Shout } from '~/graphql/schema/core.gen'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
type LoadMoreProps = {
loadFunction: (offset?: number) => void
pageSize: number
children: JSX.Element
}
type Items = Shout[] | Author[] | Reaction[]
export const LoadMoreWrapper = (props: LoadMoreProps) => {
const { t } = useLocalize()
const [items, setItems] = createSignal<Items>([])
const [offset, setOffset] = createSignal(0)
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(true)
const [isLoading, setIsLoading] = createSignal(false)
const loadItems = async () => {
setIsLoading(true)
saveScrollPosition()
const newItems = await props.loadFunction(offset())
if (!Array.isArray(newItems)) return
setItems((prev) => [...prev, ...newItems])
setOffset((prev) => prev + props.pageSize)
setIsLoadMoreButtonVisible(newItems.length >= props.pageSize)
setIsLoading(false)
restoreScrollPosition()
}
onMount(loadItems)
return (
<>
{props.children}
<Show when={isLoadMoreButtonVisible()}>
<div class="load-more-container">
<Button
onClick={loadItems}
disabled={isLoading()}
value={t('Load more')}
title={`${items().length} ${t('loaded')}`}
/>
</div>
</Show>
</>
)
}

View File

@ -1,6 +1,6 @@
import { createLazyMemo } from '@solid-primitives/memo' import { createLazyMemo } from '@solid-primitives/memo'
import { makePersisted } from '@solid-primitives/storage' import { makePersisted } from '@solid-primitives/storage'
import { Accessor, JSX, createContext, createSignal, useContext } from 'solid-js' import { Accessor, JSX, Setter, createContext, createSignal, useContext } from 'solid-js'
import { loadFollowedShouts } from '~/graphql/api/private' import { loadFollowedShouts } from '~/graphql/api/private'
import { loadShoutsSearch as fetchShoutsSearch, getShout, loadShouts } from '~/graphql/api/public' import { loadShoutsSearch as fetchShoutsSearch, getShout, loadShouts } from '~/graphql/api/public'
import { import {
@ -37,6 +37,10 @@ type FeedContextType = {
loadTopFeed: () => Promise<void> loadTopFeed: () => Promise<void>
seen: Accessor<{ [slug: string]: number }> seen: Accessor<{ [slug: string]: number }>
addSeen: (slug: string) => void addSeen: (slug: string) => void
featuredFeed: Accessor<Shout[] | undefined>
setFeaturedFeed: Setter<Shout[]>
expoFeed: Accessor<Shout[] | undefined>
setExpoFeed: Setter<Shout[]>
} }
const FeedContext = createContext<FeedContextType>({} as FeedContextType) const FeedContext = createContext<FeedContextType>({} as FeedContextType)
@ -46,6 +50,8 @@ export const useFeed = () => useContext(FeedContext)
export const FeedProvider = (props: { children: JSX.Element }) => { export const FeedProvider = (props: { children: JSX.Element }) => {
const [sortedFeed, setSortedFeed] = createSignal<Shout[]>([]) const [sortedFeed, setSortedFeed] = createSignal<Shout[]>([])
const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({}) const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({})
const [featuredFeed, setFeaturedFeed] = createSignal<Shout[]>([])
const [expoFeed, setExpoFeed] = createSignal<Shout[]>([])
const [topFeed, setTopFeed] = createSignal<Shout[]>([]) const [topFeed, setTopFeed] = createSignal<Shout[]>([])
const [topMonthFeed, setTopMonthFeed] = createSignal<Shout[]>([]) const [topMonthFeed, setTopMonthFeed] = createSignal<Shout[]>([])
const [feedByLayout, _setFeedByLayout] = createSignal<{ [layout: string]: Shout[] }>({}) const [feedByLayout, _setFeedByLayout] = createSignal<{ [layout: string]: Shout[] }>({})
@ -236,7 +242,11 @@ export const FeedProvider = (props: { children: JSX.Element }) => {
loadTopMonthFeed, loadTopMonthFeed,
loadTopFeed, loadTopFeed,
seen, seen,
addSeen addSeen,
featuredFeed,
setFeaturedFeed,
expoFeed,
setExpoFeed
}} }}
> >
{props.children} {props.children}

View File

@ -1,11 +1,12 @@
import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router' import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router'
import { Show, Suspense, createEffect, createSignal, onMount } from 'solid-js' import { Show, createEffect } from 'solid-js'
import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { useFeed } from '~/context/feed'
import { useTopics } from '~/context/topics' import { useTopics } from '~/context/topics'
import { loadShouts, loadTopics } from '~/graphql/api/public' import { loadShouts, loadTopics } from '~/graphql/api/public'
import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen'
import { byStat } from '~/lib/sort' import { byStat } from '~/lib/sort'
import { SortFunction } from '~/types/common' import { SortFunction } from '~/types/common'
import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll'
import { HomeView, HomeViewProps } from '../components/Views/Home' import { HomeView, HomeViewProps } from '../components/Views/Home'
import { Loading } from '../components/_shared/Loading' import { Loading } from '../components/_shared/Loading'
import { PageLayout } from '../components/_shared/PageLayout' import { PageLayout } from '../components/_shared/PageLayout'
@ -13,6 +14,15 @@ import { useLocalize } from '../context/localize'
export const SHOUTS_PER_PAGE = 20 export const SHOUTS_PER_PAGE = 20
const featuredLoader = (offset?: number) => {
const SHOUTS_PER_PAGE = 20
return loadShouts({
filters: { featured: true },
limit: SHOUTS_PER_PAGE,
offset
})
}
const fetchAllTopics = async () => { const fetchAllTopics = async () => {
const allTopicsLoader = loadTopics() const allTopicsLoader = loadTopics()
return await allTopicsLoader() return await allTopicsLoader()
@ -65,66 +75,63 @@ export const route = {
} satisfies RouteDefinition } satisfies RouteDefinition
export default function HomePage(props: RouteSectionProps<HomeViewProps>) { export default function HomePage(props: RouteSectionProps<HomeViewProps>) {
const limit = 20
const { addTopics } = useTopics() const { addTopics } = useTopics()
const { t } = useLocalize() const { t } = useLocalize()
const [featuredOffset, setFeaturedOffset] = createSignal<number>(0) const {
setFeaturedFeed,
featuredFeed,
topMonthFeed,
topViewedFeed,
topCommentedFeed,
topFeed: topRatedFeed
} = useFeed()
const featuredLoader = (offset?: number) => {
const result = loadShouts({
filters: { featured: true },
limit,
offset
})
return result
}
// async ssr-friendly router-level cached data source
const data = createAsync(async (prev?: HomeViewProps) => { const data = createAsync(async (prev?: HomeViewProps) => {
const topics = props.data?.topics || (await fetchAllTopics()) const topics = props.data?.topics || (await fetchAllTopics())
const featuredShoutsLoader = featuredLoader(featuredOffset()) const offset = prev?.featuredShouts?.length || 0
const featuredShoutsLoader = featuredLoader(offset)
const loaded = await featuredShoutsLoader()
const featuredShouts = [ const featuredShouts = [
...(prev?.featuredShouts || []), ...(prev?.featuredShouts || []),
...((await featuredShoutsLoader()) || props.data?.featuredShouts || []) ...(loaded || props.data?.featuredShouts || [])
] ]
const sortFn = byStat('viewed') const sortFn = byStat('viewed')
const topViewedShouts = featuredShouts?.sort(sortFn as SortFunction<Shout>) || [] const topViewedShouts = featuredShouts.sort(sortFn as SortFunction<Shout>)
const result = { return {
...prev, ...prev,
...props.data, ...props.data,
topViewedShouts, topViewedShouts,
featuredShouts, featuredShouts,
topics topics
} }
return result
}) })
createEffect(() => data()?.topics && addTopics(data()?.topics || []))
const [canLoadMoreFeatured, setCanLoadMoreFeatured] = createSignal(true) createEffect(() => {
const loadMoreFeatured = async () => { if (data()?.topics) {
saveScrollPosition() console.debug('[routes.main] topics update')
const before = data()?.featuredShouts.length || 0 addTopics(data()?.topics || [])
featuredLoader(featuredOffset())
setFeaturedOffset((o: number) => o + limit)
const after = data()?.featuredShouts.length || 0
setTimeout(() => setCanLoadMoreFeatured((_) => before !== after), 1)
restoreScrollPosition()
} }
})
onMount(async () => await loadMoreFeatured()) const loadMoreFeatured = async (offset?: number) => {
const shoutsLoader = featuredLoader(offset)
const loaded = await shoutsLoader()
loaded && setFeaturedFeed((prev: Shout[]) => [...prev, ...loaded])
}
const SHOUTS_PER_PAGE = 20
return ( return (
<PageLayout withPadding={true} title={t('Discours')} key={'home'}> <PageLayout withPadding={true} title={t('Discours')} key="home">
<Suspense fallback={<Loading />}> <Show when={(featuredFeed() || []).length > 0} fallback={<Loading />}>
<HomeView {...(data() as HomeViewProps)} /> <LoadMoreWrapper loadFunction={loadMoreFeatured} pageSize={SHOUTS_PER_PAGE}>
<Show when={canLoadMoreFeatured()}> <HomeView
<p class="load-more-container"> featuredShouts={featuredFeed() as Shout[]}
<button class="button" onClick={loadMoreFeatured}> topMonthShouts={topMonthFeed() as Shout[]}
{t('Load more')} topViewedShouts={topViewedFeed() as Shout[]}
</button> topRatedShouts={topRatedFeed() as Shout[]}
</p> topCommentedShouts={topCommentedFeed() as Shout[]}
/>
</LoadMoreWrapper>
</Show> </Show>
</Suspense>
</PageLayout> </PageLayout>
) )
} }

View File

@ -1,7 +1,9 @@
import { RouteSectionProps, createAsync, useSearchParams } from '@solidjs/router' import { RouteSectionProps, createAsync, useSearchParams } from '@solidjs/router'
import { Client } from '@urql/core' import { Client } from '@urql/core'
import { Show, createEffect, createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors'
import { Feed } from '~/components/Views/Feed' import { Feed } from '~/components/Views/Feed'
import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
import { ReactionsProvider } from '~/context/reactions' import { ReactionsProvider } from '~/context/reactions'
@ -59,7 +61,6 @@ export default (props: RouteSectionProps<Shout[]>) => {
const { t } = useLocalize() const { t } = useLocalize()
const [offset, setOffset] = createSignal<number>(0) const [offset, setOffset] = createSignal<number>(0)
const shouts = createAsync(async () => ({ ...props.data }) || (await loadMore())) const shouts = createAsync(async () => ({ ...props.data }) || (await loadMore()))
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal<boolean>(true)
const loadMore = async () => { const loadMore = async () => {
const newOffset = offset() + SHOUTS_PER_PAGE const newOffset = offset() + SHOUTS_PER_PAGE
setOffset(newOffset) setOffset(newOffset)
@ -75,7 +76,6 @@ export default (props: RouteSectionProps<Shout[]>) => {
} }
return await fetchPublishedShouts(newOffset) return await fetchPublishedShouts(newOffset)
} }
createEffect(() => setIsLoadMoreButtonVisible(offset() < (shouts()?.length || 0)))
return ( return (
<PageLayout <PageLayout
withPadding={true} withPadding={true}
@ -83,16 +83,11 @@ export default (props: RouteSectionProps<Shout[]>) => {
key="feed" key="feed"
desc="Independent media project about culture, science, art and society with horizontal editing" desc="Independent media project about culture, science, art and society with horizontal editing"
> >
<LoadMoreWrapper loadFunction={loadMore} pageSize={AUTHORS_PER_PAGE}>
<ReactionsProvider> <ReactionsProvider>
<Feed shouts={shouts() || []} /> <Feed shouts={shouts() || []} />
</ReactionsProvider> </ReactionsProvider>
<Show when={isLoadMoreButtonVisible()}> </LoadMoreWrapper>
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show>
</PageLayout> </PageLayout>
) )
} }

View File

@ -1,5 +1,5 @@
import { action, useSearchParams } from '@solidjs/router' import { action, useSearchParams } from '@solidjs/router'
import { Show, Suspense, createEffect, createSignal, onCleanup } from 'solid-js' import { Show, createEffect, createSignal, onCleanup } from 'solid-js'
import { SearchView } from '~/components/Views/Search' import { SearchView } from '~/components/Views/Search'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
@ -48,7 +48,6 @@ export default () => {
return ( return (
<PageLayout withPadding={true} title={`${t('Discours')} :: ${t('Search')}`}> <PageLayout withPadding={true} title={`${t('Discours')} :: ${t('Search')}`}>
<Suspense fallback={<Loading />}>
<Show when={isLoaded()} fallback={<Loading />}> <Show when={isLoaded()} fallback={<Loading />}>
<Show <Show
when={searchResults().length > 0} when={searchResults().length > 0}
@ -61,7 +60,6 @@ export default () => {
<SearchView results={searchResults() as SearchResult[]} query={searchParams?.q || ''} /> <SearchView results={searchResults() as SearchResult[]} query={searchParams?.q || ''} />
</Show> </Show>
</Show> </Show>
</Suspense>
</PageLayout> </PageLayout>
) )
} }