load-more-wrapper-wip
This commit is contained in:
parent
2b7a825bc5
commit
789a7497a3
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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}
|
||||||
|
|
51
src/components/_shared/LoadMoreWrapper.tsx
Normal file
51
src/components/_shared/LoadMoreWrapper.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user