load more (Feed, Author, Topic, Home update)

This commit is contained in:
Igor Lobanov 2022-10-28 23:21:47 +02:00
parent d491ae741e
commit 25fd5bf42f
23 changed files with 299 additions and 150 deletions

View File

@ -2,7 +2,7 @@ import type { Author } from '../../graphql/types.gen'
import { AuthorCard } from './Card' import { AuthorCard } from './Card'
import './Full.scss' import './Full.scss'
export default (props: { author: Author }) => { export const AuthorFull = (props: { author: Author }) => {
return ( return (
<div class="container"> <div class="container">
<div class="row"> <div class="row">

View File

@ -21,7 +21,7 @@ interface BesideProps {
iconButton?: boolean iconButton?: boolean
} }
export default (props: BesideProps) => { export const Beside = (props: BesideProps) => {
return ( return (
<Show when={!!props.beside?.slug && props.values?.length > 0}> <Show when={!!props.beside?.slug && props.values?.length > 0}>
<div class="floor floor--9"> <div class="floor floor--9">

View File

@ -1,7 +1,7 @@
import { For, Suspense } from 'solid-js/web' import { For, Suspense } from 'solid-js/web'
import OneWide from './Row1' import { Row1 } from './Row1'
import Row2 from './Row2' import { Row2 } from './Row2'
import Row3 from './Row3' import { Row3 } from './Row3'
import { shuffle } from '../../utils' import { shuffle } from '../../utils'
import { createMemo, createSignal } from 'solid-js' import { createMemo, createSignal } from 'solid-js'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
@ -10,7 +10,7 @@ import './List.scss'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
export const Block6 = (props: { articles: Shout[] }) => { export const Block6 = (props: { articles: Shout[] }) => {
const dice = createMemo(() => shuffle([OneWide, Row2, Row3])) const dice = createMemo(() => shuffle([Row1, Row2, Row3]))
return ( return (
<> <>

View File

@ -2,7 +2,7 @@ import { Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './Card'
export default (props: { article: Shout }) => ( export const Row1 = (props: { article: Shout }) => (
<Show when={!!props.article}> <Show when={!!props.article}>
<div class="floor floor--one-article"> <div class="floor floor--one-article">
<div class="wide-container row"> <div class="wide-container row">

View File

@ -2,13 +2,14 @@ import { createComputed, createSignal, Show } from 'solid-js'
import { For } from 'solid-js/web' import { For } from 'solid-js/web'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './Card'
const x = [ const x = [
['6', '6'], ['6', '6'],
['4', '8'], ['4', '8'],
['8', '4'] ['8', '4']
] ]
export default (props: { articles: Shout[] }) => { export const Row2 = (props: { articles: Shout[] }) => {
const [y, setY] = createSignal(0) const [y, setY] = createSignal(0)
createComputed(() => setY(Math.floor(Math.random() * x.length))) createComputed(() => setY(Math.floor(Math.random() * x.length)))

View File

@ -2,7 +2,7 @@ import { For } from 'solid-js/web'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './Card' import { ArticleCard } from './Card'
export default (props: { articles: Shout[]; header?: any }) => { export const Row3 = (props: { articles: Shout[]; header?: any }) => {
return ( return (
<div class="floor"> <div class="floor">
<div class="wide-container row"> <div class="wide-container row">

View File

@ -1,8 +1,8 @@
import { MainLayout } from '../Layouts/MainLayout' import { MainLayout } from '../Layouts/MainLayout'
import { AuthorView } from '../Views/Author' import { AuthorView, PRERENDERED_ARTICLES_COUNT } from '../Views/Author'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { loadArticlesForAuthors, resetSortedArticles } from '../../stores/zine/articles' import { loadAuthorArticles, resetSortedArticles } from '../../stores/zine/articles'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { loadAuthor } from '../../stores/zine/authors' import { loadAuthor } from '../../stores/zine/authors'
import { Loading } from '../Loading' import { Loading } from '../Loading'
@ -27,7 +27,7 @@ export const AuthorPage = (props: PageProps) => {
return return
} }
await loadArticlesForAuthors({ authorSlugs: [slug()] }) await loadAuthorArticles({ authorSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT })
await loadAuthor({ slug: slug() }) await loadAuthor({ slug: slug() })
setIsLoaded(true) setIsLoaded(true)

View File

@ -1,30 +1,14 @@
import { MainLayout } from '../Layouts/MainLayout' import { MainLayout } from '../Layouts/MainLayout'
import { FeedView } from '../Views/Feed' import { FeedView } from '../Views/Feed'
import type { PageProps } from '../types' import { onCleanup } from 'solid-js'
import { createSignal, onCleanup, onMount, Show } from 'solid-js' import { resetSortedArticles } from '../../stores/zine/articles'
import { loadRecentArticles, resetSortedArticles } from '../../stores/zine/articles'
import { Loading } from '../Loading'
export const FeedPage = (props: PageProps) => {
const [isLoaded, setIsLoaded] = createSignal(Boolean(props.feedArticles))
onMount(async () => {
if (isLoaded()) {
return
}
await loadRecentArticles({ limit: 50, offset: 0 })
setIsLoaded(true)
})
export const FeedPage = () => {
onCleanup(() => resetSortedArticles()) onCleanup(() => resetSortedArticles())
return ( return (
<MainLayout> <MainLayout>
<Show when={isLoaded()} fallback={<Loading />}> <FeedView />
<FeedView articles={props.feedArticles} />
</Show>
</MainLayout> </MainLayout>
) )
} }

View File

@ -1,4 +1,4 @@
import { HomeView } from '../Views/Home' import { HomeView, PRERENDERED_ARTICLES_COUNT } from '../Views/Home'
import { MainLayout } from '../Layouts/MainLayout' import { MainLayout } from '../Layouts/MainLayout'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createSignal, onCleanup, onMount, Show } from 'solid-js' import { createSignal, onCleanup, onMount, Show } from 'solid-js'
@ -14,7 +14,7 @@ export const HomePage = (props: PageProps) => {
return return
} }
await loadPublishedArticles({ limit: 5, offset: 0 }) await loadPublishedArticles({ limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
await loadRandomTopics() await loadRandomTopics()
setIsLoaded(true) setIsLoaded(true)

View File

@ -1,8 +1,8 @@
import { MainLayout } from '../Layouts/MainLayout' import { MainLayout } from '../Layouts/MainLayout'
import { TopicView } from '../Views/Topic' import { PRERENDERED_ARTICLES_COUNT, TopicView } from '../Views/Topic'
import type { PageProps } from '../types' import type { PageProps } from '../types'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { loadArticlesForTopics, resetSortedArticles } from '../../stores/zine/articles' import { loadTopicArticles, resetSortedArticles } from '../../stores/zine/articles'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { loadTopic } from '../../stores/zine/topics' import { loadTopic } from '../../stores/zine/topics'
import { Loading } from '../Loading' import { Loading } from '../Loading'
@ -27,7 +27,7 @@ export const TopicPage = (props: PageProps) => {
return return
} }
await loadArticlesForTopics({ topicSlugs: [slug()] }) await loadTopicArticles({ topicSlug: slug(), limit: PRERENDERED_ARTICLES_COUNT, offset: 0 })
await loadTopic({ slug: slug() }) await loadTopic({ slug: slug() })
setIsLoaded(true) setIsLoaded(true)

View File

@ -1,17 +1,18 @@
import { Show, createMemo } from 'solid-js' import { Show, createMemo, createSignal, For, onMount } from 'solid-js'
import type { Author, Shout } from '../../graphql/types.gen' import type { Author, Shout } from '../../graphql/types.gen'
import Row2 from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
import Row3 from '../Feed/Row3' import { Row3 } from '../Feed/Row3'
// import Beside from '../Feed/Beside' import { AuthorFull } from '../Author/Full'
import AuthorFull from '../Author/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { useArticlesStore } from '../../stores/zine/articles' import { loadAuthorArticles, useArticlesStore } from '../../stores/zine/articles'
import '../../styles/Topic.scss' import '../../styles/Topic.scss'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import Beside from '../Feed/Beside' import { Beside } from '../Feed/Beside'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
// TODO: load reactions on client // TODO: load reactions on client
type AuthorProps = { type AuthorProps = {
@ -26,16 +27,37 @@ type AuthorPageSearchParams = {
by: '' | 'viewed' | 'rating' | 'commented' | 'recent' by: '' | 'viewed' | 'rating' | 'commented' | 'recent'
} }
export const PRERENDERED_ARTICLES_COUNT = 12
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
export const AuthorView = (props: AuthorProps) => { export const AuthorView = (props: AuthorProps) => {
const { sortedArticles } = useArticlesStore({ const { sortedArticles } = useArticlesStore({
sortedArticles: props.authorArticles sortedArticles: props.authorArticles
}) })
const { authorEntities } = useAuthorsStore({ authors: [props.author] }) const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const { topicsByAuthor } = useTopicsStore() const { topicsByAuthor } = useTopicsStore()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const author = createMemo(() => authorEntities()[props.authorSlug]) const author = createMemo(() => authorEntities()[props.authorSlug])
const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>() const { searchParams, changeSearchParam } = useRouter<AuthorPageSearchParams>()
const loadMore = async () => {
saveScrollPosition()
const { hasMore } = await loadAuthorArticles({
authorSlug: author().slug,
limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length
})
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition()
}
onMount(async () => {
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore()
}
})
const title = createMemo(() => { const title = createMemo(() => {
const m = searchParams().by const m = searchParams().by
if (m === 'viewed') return t('Top viewed') if (m === 'viewed') return t('Top viewed')
@ -44,6 +66,10 @@ export const AuthorView = (props: AuthorProps) => {
return t('Top recent') return t('Top recent')
}) })
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
return ( return (
<div class="container author-page"> <div class="container author-page">
<Show when={author()} fallback={<div class="center">{t('Loading')}</div>}> <Show when={author()} fallback={<div class="center">{t('Loading')}</div>}>
@ -83,8 +109,8 @@ export const AuthorView = (props: AuthorProps) => {
</div> </div>
<h3 class="col-12">{title()}</h3> <h3 class="col-12">{title()}</h3>
<div class="row"> <div class="row">
<Show when={sortedArticles().length > 0}>
<Beside <Beside
title={t('Topics which supported by author')} title={t('Topics which supported by author')}
values={topicsByAuthor()[author().slug].slice(0, 5)} values={topicsByAuthor()[author().slug].slice(0, 5)}
@ -96,18 +122,26 @@ export const AuthorView = (props: AuthorProps) => {
iconButton={true} iconButton={true}
/> />
<Row3 articles={sortedArticles().slice(1, 4)} /> <Row3 articles={sortedArticles().slice(1, 4)} />
<Show when={sortedArticles().length > 4}>
<Row2 articles={sortedArticles().slice(4, 6)} /> <Row2 articles={sortedArticles().slice(4, 6)} />
</Show>
<Show when={sortedArticles().length > 6}>
<Row3 articles={sortedArticles().slice(6, 9)} /> <Row3 articles={sortedArticles().slice(6, 9)} />
</Show>
<Show when={sortedArticles().length > 9}>
<Row3 articles={sortedArticles().slice(9, 12)} /> <Row3 articles={sortedArticles().slice(9, 12)} />
</Show>
<For each={pages()}>
{(page) => (
<>
<Row3 articles={page.slice(0, 3)} />
<Row3 articles={page.slice(3, 6)} />
<Row3 articles={page.slice(6, 9)} />
</>
)}
</For>
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show> </Show>
</div> </div>
</Show> </Show>

View File

@ -1,5 +1,4 @@
import { createMemo, For, Show } from 'solid-js' import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Shout, Reaction } from '../../graphql/types.gen'
import '../../styles/Feed.scss' import '../../styles/Feed.scss'
import stylesBeside from '../../components/Feed/Beside.module.scss' import stylesBeside from '../../components/Feed/Beside.module.scss'
import { Icon } from '../Nav/Icon' import { Icon } from '../Nav/Icon'
@ -17,11 +16,6 @@ import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
interface FeedProps {
articles: Shout[]
reactions?: Reaction[]
}
// const AUTHORSHIP_REACTIONS = [ // const AUTHORSHIP_REACTIONS = [
// ReactionKind.Accept, // ReactionKind.Accept,
// ReactionKind.Reject, // ReactionKind.Reject,
@ -29,9 +23,11 @@ interface FeedProps {
// ReactionKind.Ask // ReactionKind.Ask
// ] // ]
export const FeedView = (props: FeedProps) => { export const FEED_PAGE_SIZE = 20
export const FeedView = () => {
// state // state
const { sortedArticles } = useArticlesStore({ sortedArticles: props.articles }) const { sortedArticles } = useArticlesStore()
const reactions = useReactionsStore() const reactions = useReactionsStore()
const { sortedAuthors } = useAuthorsStore() const { sortedAuthors } = useAuthorsStore()
const { topTopics } = useTopicsStore() const { topTopics } = useTopicsStore()
@ -40,6 +36,8 @@ export const FeedView = (props: FeedProps) => {
const topReactions = createMemo(() => sortBy(reactions(), byCreated)) const topReactions = createMemo(() => sortBy(reactions(), byCreated))
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
// const expectingFocus = createMemo<Shout[]>(() => { // const expectingFocus = createMemo<Shout[]>(() => {
// // 1 co-author notifications needs // // 1 co-author notifications needs
// // TODO: list of articles where you are co-author // // TODO: list of articles where you are co-author
@ -53,13 +51,15 @@ export const FeedView = (props: FeedProps) => {
// return [] // return []
// }) // })
// eslint-disable-next-line unicorn/consistent-function-scoping const loadMore = async () => {
const loadMore = () => { const { hasMore } = await loadRecentArticles({ limit: FEED_PAGE_SIZE, offset: sortedArticles().length })
// const limit = props.limit || 50 setIsLoadMoreButtonVisible(hasMore)
// const offset = props.offset || 0
// FIXME
loadRecentArticles({ limit: 50, offset: 0 })
} }
onMount(() => {
loadMore()
})
return ( return (
<> <>
<div class="container feed"> <div class="container feed">
@ -113,10 +113,6 @@ export const FeedView = (props: FeedProps) => {
{(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />} {(article) => <ArticleCard article={article} settings={{ isFeedMode: true }} />}
</For> </For>
</Show> </Show>
<p class="load-more-container">
<button class="button">{t('Load more')}</button>
</p>
</div> </div>
<aside class="col-md-3"> <aside class="col-md-3">
@ -136,12 +132,13 @@ export const FeedView = (props: FeedProps) => {
</Show> </Show>
</aside> </aside>
</div> </div>
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container"> <p class="load-more-container">
<button class="button" onClick={loadMore}> <button class="button" onClick={loadMore}>
{t('Load more')} {t('Load more')}
</button> </button>
</p> </p>
</Show>
</div> </div>
</> </>
) )

View File

@ -1,12 +1,12 @@
import { createMemo, For, onMount, Show } from 'solid-js' import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import Banner from '../Discours/Banner' import Banner from '../Discours/Banner'
import { NavTopics } from '../Nav/Topics' import { NavTopics } from '../Nav/Topics'
import { Row5 } from '../Feed/Row5' import { Row5 } from '../Feed/Row5'
import Row3 from '../Feed/Row3' import { Row3 } from '../Feed/Row3'
import Row2 from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
import Row1 from '../Feed/Row1' import { Row1 } from '../Feed/Row1'
import Hero from '../Discours/Hero' import Hero from '../Discours/Hero'
import Beside from '../Feed/Beside' import { Beside } from '../Feed/Beside'
import RowShort from '../Feed/RowShort' import RowShort from '../Feed/RowShort'
import Slider from '../Feed/Slider' import Slider from '../Feed/Slider'
import Group from '../Feed/Group' import Group from '../Feed/Group'
@ -24,6 +24,7 @@ import {
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
const log = getLogger('home view') const log = getLogger('home view')
@ -31,7 +32,8 @@ type HomeProps = {
randomTopics: Topic[] randomTopics: Topic[]
recentPublishedArticles: Shout[] recentPublishedArticles: Shout[]
} }
const PRERENDERED_ARTICLES_COUNT = 5
export const PRERENDERED_ARTICLES_COUNT = 5
const CLIENT_LOAD_ARTICLES_COUNT = 29 const CLIENT_LOAD_ARTICLES_COUNT = 29
const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3 const LOAD_MORE_PAGE_SIZE = 16 // Row1 + Row3 + Row2 + Beside (3 + 1) + Row1 + Row 2 + Row3
@ -49,14 +51,20 @@ export const HomeView = (props: HomeProps) => {
const { randomTopics, topTopics } = useTopicsStore({ const { randomTopics, topTopics } = useTopicsStore({
randomTopics: props.randomTopics randomTopics: props.randomTopics
}) })
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { topAuthors } = useTopAuthorsStore() const { topAuthors } = useTopAuthorsStore()
onMount(() => { onMount(async () => {
loadTopArticles() loadTopArticles()
loadTopMonthArticles() loadTopMonthArticles()
if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) { if (sortedArticles().length < PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) {
loadPublishedArticles({ limit: CLIENT_LOAD_ARTICLES_COUNT, offset: sortedArticles().length }) const { hasMore } = await loadPublishedArticles({
limit: CLIENT_LOAD_ARTICLES_COUNT,
offset: sortedArticles().length
})
setIsLoadMoreButtonVisible(hasMore)
} }
}) })
@ -85,22 +93,23 @@ export const HomeView = (props: HomeProps) => {
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
await loadPublishedArticles({ limit: LOAD_MORE_PAGE_SIZE, offset: sortedArticles().length })
const { hasMore } = await loadPublishedArticles({
limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length
})
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition() restoreScrollPosition()
} }
const pages = createMemo<Shout[][]>(() => { const pages = createMemo<Shout[][]>(() =>
return sortedArticles() splitToPages(
.slice(PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT) sortedArticles(),
.reduce((acc, article, index) => { PRERENDERED_ARTICLES_COUNT + CLIENT_LOAD_ARTICLES_COUNT,
if (index % LOAD_MORE_PAGE_SIZE === 0) { LOAD_MORE_PAGE_SIZE
acc.push([]) )
} )
acc[acc.length - 1].push(article)
return acc
}, [] as Shout[][])
})
return ( return (
<Show when={locale() && sortedArticles().length > 0}> <Show when={locale() && sortedArticles().length > 0}>
@ -173,11 +182,13 @@ export const HomeView = (props: HomeProps) => {
)} )}
</For> </For>
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container"> <p class="load-more-container">
<button class="button" onClick={loadMore}> <button class="button" onClick={loadMore}>
{t('Load more')} {t('Load more')}
</button> </button>
</p> </p>
</Show> </Show>
</Show>
) )
} }

View File

@ -1,16 +1,18 @@
import { For, Show, createMemo } from 'solid-js' import { For, Show, createMemo, onMount, createSignal } from 'solid-js'
import type { Shout, Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/types.gen'
import Row3 from '../Feed/Row3' import { Row3 } from '../Feed/Row3'
import Row2 from '../Feed/Row2' import { Row2 } from '../Feed/Row2'
import Beside from '../Feed/Beside' import { Beside } from '../Feed/Beside'
import { ArticleCard } from '../Feed/Card' import { ArticleCard } from '../Feed/Card'
import '../../styles/Topic.scss' import '../../styles/Topic.scss'
import { FullTopic } from '../Topic/Full' import { FullTopic } from '../Topic/Full'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { useArticlesStore } from '../../stores/zine/articles' import { loadPublishedArticles, useArticlesStore } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors' import { useAuthorsStore } from '../../stores/zine/authors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages'
type TopicsPageSearchParams = { type TopicsPageSearchParams = {
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented' by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
@ -22,9 +24,14 @@ interface TopicProps {
topicSlug: string topicSlug: string
} }
export const PRERENDERED_ARTICLES_COUNT = 21
const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3
export const TopicView = (props: TopicProps) => { export const TopicView = (props: TopicProps) => {
const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>() const { searchParams, changeSearchParam } = useRouter<TopicsPageSearchParams>()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles }) const { sortedArticles } = useArticlesStore({ sortedArticles: props.topicArticles })
const { topicEntities } = useTopicsStore({ topics: [props.topic] }) const { topicEntities } = useTopicsStore({ topics: [props.topic] })
@ -32,6 +39,24 @@ export const TopicView = (props: TopicProps) => {
const topic = createMemo(() => topicEntities()[props.topicSlug]) const topic = createMemo(() => topicEntities()[props.topicSlug])
const loadMore = async () => {
saveScrollPosition()
const { hasMore } = await loadPublishedArticles({
limit: LOAD_MORE_PAGE_SIZE,
offset: sortedArticles().length
})
setIsLoadMoreButtonVisible(hasMore)
restoreScrollPosition()
}
onMount(async () => {
if (sortedArticles().length === PRERENDERED_ARTICLES_COUNT) {
loadMore()
}
})
const title = createMemo(() => { const title = createMemo(() => {
const m = searchParams().by const m = searchParams().by
if (m === 'viewed') return t('Top viewed') if (m === 'viewed') return t('Top viewed')
@ -40,6 +65,10 @@ export const TopicView = (props: TopicProps) => {
return t('Top recent') return t('Top recent')
}) })
const pages = createMemo<Shout[][]>(() =>
splitToPages(sortedArticles(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE)
)
return ( return (
<div class="topic-page container"> <div class="topic-page container">
<Show when={topic()}> <Show when={topic()}>
@ -110,6 +139,24 @@ export const TopicView = (props: TopicProps) => {
<Row3 articles={sortedArticles().slice(15, 18)} /> <Row3 articles={sortedArticles().slice(15, 18)} />
<Row3 articles={sortedArticles().slice(18, 21)} /> <Row3 articles={sortedArticles().slice(18, 21)} />
</Show> </Show>
<For each={pages()}>
{(page) => (
<>
<Row3 articles={page.slice(0, 3)} />
<Row3 articles={page.slice(3, 6)} />
<Row3 articles={page.slice(6, 9)} />
</>
)}
</For>
<Show when={isLoadMoreButtonVisible()}>
<p class="load-more-container">
<button class="button" onClick={loadMore}>
{t('Load more')}
</button>
</p>
</Show>
</div> </div>
</Show> </Show>
</div> </div>

View File

@ -8,7 +8,6 @@ export type PageProps = {
authorArticles?: Shout[] authorArticles?: Shout[]
topicArticles?: Shout[] topicArticles?: Shout[]
homeArticles?: Shout[] homeArticles?: Shout[]
feedArticles?: Shout[]
author?: Author author?: Author
allAuthors?: Author[] allAuthors?: Author[]
topic?: Topic topic?: Topic

View File

@ -3,9 +3,10 @@ import { Root } from '../../../components/Root'
import Zine from '../../../layouts/zine.astro' import Zine from '../../../layouts/zine.astro'
import { apiClient } from '../../../utils/apiClient' import { apiClient } from '../../../utils/apiClient'
import { initRouter } from '../../../stores/router' import { initRouter } from '../../../stores/router'
import { PRERENDERED_ARTICLES_COUNT } from '../../../components/Views/Author'
const slug = Astro.params.slug.toString() const slug = Astro.params.slug.toString()
const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: 50 }) const articles = await apiClient.getArticlesForAuthors({ authorSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
const author = articles[0].authors.find((a) => a.slug === slug) const author = articles[0].authors.find((a) => a.slug === slug)
const { pathname, search } = Astro.url const { pathname, search } = Astro.url

View File

@ -1,16 +1,12 @@
--- ---
import { Root } from '../../components/Root' import { Root } from '../../components/Root'
import Zine from '../../layouts/zine.astro' import Zine from '../../layouts/zine.astro'
import { apiClient } from '../../utils/apiClient'
import { initRouter } from '../../stores/router' import { initRouter } from '../../stores/router'
const { pathname, search } = Astro.url const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)
const articles = await apiClient.getRecentArticles({ limit: 50 })
--- ---
<Zine> <Zine>
<Root feedArticles={articles} client:load /> <Root client:load />
</Zine> </Zine>

View File

@ -3,14 +3,14 @@ import Zine from '../layouts/zine.astro'
import { Root } from '../components/Root' import { Root } from '../components/Root'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
import { initRouter } from '../stores/router' import { initRouter } from '../stores/router'
import { PRERENDERED_ARTICLES_COUNT } from '../components/Views/Home'
const randomTopics = await apiClient.getRandomTopics({ amount: 12 }) const randomTopics = await apiClient.getRandomTopics({ amount: 12 })
const articles = await apiClient.getRecentPublishedArticles({ limit: 5 }) const articles = await apiClient.getRecentPublishedArticles({ limit: PRERENDERED_ARTICLES_COUNT })
const { pathname, search } = Astro.url const { pathname, search } = Astro.url
initRouter(pathname, search) initRouter(pathname, search)
Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate') Astro.response.headers.set('Cache-Control', 's-maxage=1, stale-while-revalidate')
--- ---

View File

@ -2,9 +2,10 @@
import { Root } from '../../components/Root' import { Root } from '../../components/Root'
import Zine from '../../layouts/zine.astro' import Zine from '../../layouts/zine.astro'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import { PRERENDERED_ARTICLES_COUNT } from '../../components/Views/Topic'
const slug = Astro.params.slug?.toString() || '' const slug = Astro.params.slug?.toString() || ''
const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: 50 }) const articles = await apiClient.getArticlesForTopics({ topicSlugs: [slug], limit: PRERENDERED_ARTICLES_COUNT })
const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug) const topic = articles[0].topics.find(({ slug: topicSlug }) => topicSlug === slug)
import { initRouter } from '../../stores/router' import { initRouter } from '../../stores/router'

View File

@ -127,40 +127,109 @@ const addSortedArticles = (articles: Shout[]) => {
setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles]) setSortedArticles((prevSortedArticles) => [...prevSortedArticles, ...articles])
} }
export const loadFeed = async ({
limit,
offset
}: {
limit: number
offset?: number
}): Promise<{ hasMore: boolean }> => {
// TODO: load actual feed
return await loadRecentArticles({ limit, offset })
}
export const loadRecentArticles = async ({ export const loadRecentArticles = async ({
limit, limit,
offset offset
}: { }: {
limit?: number limit: number
offset?: number offset?: number
}): Promise<void> => { }): Promise<{ hasMore: boolean }> => {
const newArticles = await apiClient.getRecentArticles({ limit, offset }) const newArticles = await apiClient.getRecentArticles({ limit: limit + 1, offset })
const hasMore = newArticles.length === limit + 1
if (hasMore) {
newArticles.splice(-1)
}
addArticles(newArticles) addArticles(newArticles)
addSortedArticles(newArticles) addSortedArticles(newArticles)
return { hasMore }
} }
export const loadPublishedArticles = async ({ export const loadPublishedArticles = async ({
limit, limit,
offset offset = 0
}: { }: {
limit?: number limit: number
offset?: number offset?: number
}): Promise<void> => { }): Promise<{ hasMore: boolean }> => {
const newArticles = await apiClient.getPublishedArticles({ limit, offset }) const newArticles = await apiClient.getPublishedArticles({ limit: limit + 1, offset })
const hasMore = newArticles.length === limit + 1
if (hasMore) {
newArticles.splice(-1)
}
addArticles(newArticles) addArticles(newArticles)
addSortedArticles(newArticles) addSortedArticles(newArticles)
return { hasMore }
} }
export const loadArticlesForAuthors = async ({ authorSlugs }: { authorSlugs: string[] }): Promise<void> => { export const loadAuthorArticles = async ({
const articles = await apiClient.getArticlesForAuthors({ authorSlugs, limit: 50 }) authorSlug,
addArticles(articles) limit,
setSortedArticles(articles) offset = 0
}: {
authorSlug: string
limit: number
offset?: number
}): Promise<{ hasMore: boolean }> => {
const newArticles = await apiClient.getArticlesForAuthors({
authorSlugs: [authorSlug],
limit: limit + 1,
offset
})
const hasMore = newArticles.length === limit + 1
if (hasMore) {
newArticles.splice(-1)
}
addArticles(newArticles)
addSortedArticles(newArticles)
return { hasMore }
} }
export const loadArticlesForTopics = async ({ topicSlugs }: { topicSlugs: string[] }): Promise<void> => { export const loadTopicArticles = async ({
const articles = await apiClient.getArticlesForTopics({ topicSlugs, limit: 50 }) topicSlug,
addArticles(articles) limit,
setSortedArticles(articles) offset
}: {
topicSlug: string
limit: number
offset: number
}): Promise<{ hasMore: boolean }> => {
const newArticles = await apiClient.getArticlesForTopics({
topicSlugs: [topicSlug],
limit: limit + 1,
offset
})
const hasMore = newArticles.length === limit + 1
if (hasMore) {
newArticles.splice(-1)
}
addArticles(newArticles)
addSortedArticles(newArticles)
return { hasMore }
} }
export const resetSortedArticles = () => { export const resetSortedArticles = () => {

View File

@ -1,6 +1,5 @@
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { byCreated, byStat, byTopicStatDesc } from '../../utils/sortby'
import { getLogger } from '../../utils/logger' import { getLogger } from '../../utils/logger'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'

View File

@ -184,7 +184,7 @@ export const apiClient = {
}, },
getArticlesForTopics: async ({ getArticlesForTopics: async ({
topicSlugs, topicSlugs,
limit = FEED_SIZE, limit,
offset = 0 offset = 0
}: { }: {
topicSlugs: string[] topicSlugs: string[]
@ -207,7 +207,7 @@ export const apiClient = {
}, },
getArticlesForAuthors: async ({ getArticlesForAuthors: async ({
authorSlugs, authorSlugs,
limit = FEED_SIZE, limit,
offset = 0 offset = 0
}: { }: {
authorSlugs: string[] authorSlugs: string[]

10
src/utils/splitToPages.ts Normal file
View File

@ -0,0 +1,10 @@
export function splitToPages<T>(arr: T[], startIndex: number, pageSize: number): T[][] {
return arr.slice(startIndex).reduce((acc, article, index) => {
if (index % pageSize === 0) {
acc.push([])
}
acc[acc.length - 1].push(article)
return acc
}, [] as T[][])
}