From 4fe2768329d62a521d5637079d1c362b545a34e2 Mon Sep 17 00:00:00 2001 From: Untone Date: Mon, 15 Jul 2024 23:35:33 +0300 Subject: [PATCH] load-more-main-ok --- .../Feed/Placeholder/Placeholder.tsx | 4 +- src/components/Nav/Header/Header.tsx | 4 +- src/components/Views/Feed/Feed.tsx | 53 +++----- src/components/_shared/LoadMoreWrapper.tsx | 10 +- src/context/feed.tsx | 12 +- src/intl/locales/ru/translation.json | 1 + src/routes/(main).tsx | 49 +++----- .../feed/{(feed).tsx => [...order].tsx} | 73 +++++++---- src/routes/feed/my/[...mode]/[...order].tsx | 114 ++++++++++++++++++ 9 files changed, 218 insertions(+), 102 deletions(-) rename src/routes/feed/{(feed).tsx => [...order].tsx} (50%) create mode 100644 src/routes/feed/my/[...mode]/[...order].tsx diff --git a/src/components/Feed/Placeholder/Placeholder.tsx b/src/components/Feed/Placeholder/Placeholder.tsx index 26569bba..a1f60fd3 100644 --- a/src/components/Feed/Placeholder/Placeholder.tsx +++ b/src/components/Feed/Placeholder/Placeholder.tsx @@ -51,7 +51,7 @@ const data: PlaceholderData = { text: 'Placeholder feedDiscussions', buttonLabelAuthor: 'Current discussions', buttonLabelFeed: 'Enter', - href: '/feed?by=last_comment' + href: '/feed/hot' }, author: { image: 'placeholder-join.webp', @@ -71,7 +71,7 @@ const data: PlaceholderData = { header: 'Join discussions', text: 'Placeholder feedDiscussions', buttonLabel: 'Go to discussions', - href: '/feed?by=last_comment', + href: '/feed/hot', profileLinks: [ { href: '/debate', diff --git a/src/components/Nav/Header/Header.tsx b/src/components/Nav/Header/Header.tsx index 5b65f7e1..aae2e336 100644 --- a/src/components/Nav/Header/Header.tsx +++ b/src/components/Nav/Header/Header.tsx @@ -500,14 +500,14 @@ export const Header = (props: Props) => { -
  • + {/*
  • {t('Bookmarks')} -
  • + */} diff --git a/src/components/Views/Feed/Feed.tsx b/src/components/Views/Feed/Feed.tsx index 00f64d31..69d32729 100644 --- a/src/components/Views/Feed/Feed.tsx +++ b/src/components/Views/Feed/Feed.tsx @@ -1,6 +1,6 @@ import { A, createAsync, useLocation, useNavigate, useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' -import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' +import { For, Show, createEffect, createMemo, createSignal, on } from 'solid-js' import { DropDown } from '~/components/_shared/DropDown' import { Option } from '~/components/_shared/DropDown/DropDown' import { Icon } from '~/components/_shared/Icon' @@ -18,7 +18,7 @@ import { useUI } from '~/context/ui' import { loadUnratedShouts } from '~/graphql/api/private' import type { Author, Reaction, Shout } from '~/graphql/schema/core.gen' import { byCreated } from '~/lib/sort' -import { FeedSearchParams } from '~/routes/feed/(feed)' +import { FeedSearchParams } from '~/routes/feed/[...order]' import { CommentDate } from '../../Article/CommentDate' import { getShareUrl } from '../../Article/SharePopup' import { AuthorBadge } from '../../Author/AuthorBadge' @@ -36,6 +36,7 @@ export type PeriodType = 'week' | 'month' | 'year' export type FeedProps = { shouts?: Shout[] + mode?: '' | 'likes' | 'hot' } export const FeedView = (props: FeedProps) => { @@ -53,7 +54,7 @@ export const FeedView = (props: FeedProps) => { const [isLoading, setIsLoading] = createSignal(false) const [isRightColumnLoaded, setIsRightColumnLoaded] = createSignal(false) const { session } = useSession() - const { nonfeaturedFeed, setNonFeaturedFeed } = useFeed() + const { feed, setFeed } = useFeed() const { loadReactionsBy } = useReactions() const { topTopics } = useTopics() const { topAuthors } = useAuthors() @@ -67,20 +68,13 @@ export const FeedView = (props: FeedProps) => { setTopComments(comments.sort(byCreated).reverse()) } - onMount( - () => - props.shouts && - Array.isArray(props.shouts) && - setNonFeaturedFeed((prev) => [...prev, ...(props.shouts || [])]) && console.info(nonfeaturedFeed()) - ) - createEffect( on( - () => nonfeaturedFeed(), + feed, (sss?: Shout[]) => { if (sss && Array.isArray(sss)) { setIsLoading(true) - setNonFeaturedFeed((prev) => [...prev, ...sss]) + setFeed((prev) => [...prev, ...sss]) Promise.all([ loadTopComments(), loadReactionsBy({ by: { shouts: sss.map((s: Shout) => s.slug) } }) @@ -113,40 +107,33 @@ export const FeedView = (props: FeedProps) => { - +
    - + { navigate(`/feed/${mode.value}`)} @@ -166,8 +153,8 @@ export const FeedView = (props: FeedProps) => {
    }> - 0}> - + 0}> + {(article) => ( handleShare(shared)} @@ -199,7 +186,7 @@ export const FeedView = (props: FeedProps) => {
    - + {(article) => ( )} diff --git a/src/components/_shared/LoadMoreWrapper.tsx b/src/components/_shared/LoadMoreWrapper.tsx index 43403fba..130a2401 100644 --- a/src/components/_shared/LoadMoreWrapper.tsx +++ b/src/components/_shared/LoadMoreWrapper.tsx @@ -4,17 +4,17 @@ import { useLocalize } from '~/context/localize' import { Author, Reaction, Shout } from '~/graphql/schema/core.gen' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' +export type LoadMoreItems = Shout[] | Author[] | Reaction[] + type LoadMoreProps = { - loadFunction: (offset?: number) => void + loadFunction: (offset?: number) => Promise pageSize: number children: JSX.Element } -type Items = Shout[] | Author[] | Reaction[] - export const LoadMoreWrapper = (props: LoadMoreProps) => { const { t } = useLocalize() - const [items, setItems] = createSignal([]) + const [items, setItems] = createSignal([]) const [offset, setOffset] = createSignal(0) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(true) const [isLoading, setIsLoading] = createSignal(false) @@ -25,7 +25,7 @@ export const LoadMoreWrapper = (props: LoadMoreProps) => { const newItems = await props.loadFunction(offset()) if (!Array.isArray(newItems)) return console.debug('[_share] load more items', newItems) - setItems((prev) => [...prev, ...newItems]) + setItems((prev) => [...prev, ...newItems] as LoadMoreItems) setOffset((prev) => prev + props.pageSize) setIsLoadMoreButtonVisible(newItems.length >= props.pageSize - 1) setIsLoading(false) diff --git a/src/context/feed.tsx b/src/context/feed.tsx index cb055318..7d1617aa 100644 --- a/src/context/feed.tsx +++ b/src/context/feed.tsx @@ -34,9 +34,9 @@ type FeedContextType = { seen: Accessor<{ [slug: string]: number }> addSeen: (slug: string) => void - // featured - nonfeaturedFeed: Accessor - setNonFeaturedFeed: Setter + // all + feed: Accessor + setFeed: Setter // featured featuredFeed: Accessor @@ -62,7 +62,7 @@ export const useFeed = () => useContext(FeedContext) export const FeedProvider = (props: { children: JSX.Element }) => { const [sortedFeed, setSortedFeed] = createSignal([]) const [articleEntities, setArticleEntities] = createSignal<{ [articleSlug: string]: Shout }>({}) - const [nonfeaturedFeed, setNonFeaturedFeed] = createSignal([]) + const [feed, setFeed] = createSignal([]) const [featuredFeed, setFeaturedFeed] = createSignal([]) const [expoFeed, setExpoFeed] = createSignal([]) const [topFeed, setTopFeed] = createSignal([]) @@ -260,8 +260,8 @@ export const FeedProvider = (props: { children: JSX.Element }) => { setFeaturedFeed, expoFeed, setExpoFeed, - nonfeaturedFeed, - setNonFeaturedFeed + feed, + setFeed }} > {props.children} diff --git a/src/intl/locales/ru/translation.json b/src/intl/locales/ru/translation.json index 93b9aad2..2198e455 100644 --- a/src/intl/locales/ru/translation.json +++ b/src/intl/locales/ru/translation.json @@ -263,6 +263,7 @@ "Lists": "Списки", "Literature": "Литература", "Load more": "Показать ещё", + "loaded": "загружено", "Loading": "Загрузка", "Login and security": "Вход и безопасность", "Logout": "Выход", diff --git a/src/routes/(main).tsx b/src/routes/(main).tsx index d511d792..abaf23c0 100644 --- a/src/routes/(main).tsx +++ b/src/routes/(main).tsx @@ -1,12 +1,10 @@ import { type RouteDefinition, type RouteSectionProps, createAsync } from '@solidjs/router' import { Show, createEffect } from 'solid-js' -import { LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' +import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { useFeed } from '~/context/feed' import { useTopics } from '~/context/topics' import { loadShouts, loadTopics } from '~/graphql/api/public' import { LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' -import { byStat } from '~/lib/sort' -import { SortFunction } from '~/types/common' import { HomeView, HomeViewProps } from '../components/Views/Home' import { Loading } from '../components/_shared/Loading' import { PageLayout } from '../components/_shared/PageLayout' @@ -74,7 +72,6 @@ export const route = { } satisfies RouteDefinition export default function HomePage(props: RouteSectionProps) { - const { addTopics } = useTopics() const { t } = useLocalize() const { setFeaturedFeed, @@ -85,46 +82,38 @@ export default function HomePage(props: RouteSectionProps) { topFeed: topRatedFeed } = useFeed() - const data = createAsync(async (prev?: HomeViewProps) => { - const topics = props.data?.topics || (await fetchAllTopics()) - const offset = prev?.featuredShouts?.length || 0 - const featuredShoutsLoader = featuredLoader(offset) - const loaded = await featuredShoutsLoader() - setFeaturedFeed((prev) => [...prev, ...loaded||[]]) - const featuredShouts = [ - ...(prev?.featuredShouts || []), - ...(loaded || props.data?.featuredShouts || []) - ] - const sortFn = byStat('viewed') - const topViewedShouts = featuredShouts.sort(sortFn as SortFunction) - return { - ...prev, - ...props.data, - topViewedShouts, - featuredShouts, - topics - } - }) - + // preload all topics + const { addTopics, sortedTopics } = useTopics() createEffect(() => { - if (data()?.topics) { - console.debug('[routes.main] topics update') - addTopics(data()?.topics || []) - } + !sortedTopics() && props.data.topics && addTopics(props.data.topics) }) + // load more faetured shouts const loadMoreFeatured = async (offset?: number) => { const shoutsLoader = featuredLoader(offset) const loaded = await shoutsLoader() loaded && setFeaturedFeed((prev: Shout[]) => [...prev, ...loaded]) + return loaded as LoadMoreItems } + + // preload featured shouts + const shouts = createAsync(async () => { + if (props.data.featuredShouts) { + setFeaturedFeed(props.data.featuredShouts) + console.debug('[routes.main] featured feed preloaded') + return props.data.featuredShouts + } + return await loadMoreFeatured() + }) + const SHOUTS_PER_PAGE = 20 + return ( 0} fallback={}> { return Math.floor(d.getTime() / 1000) } -const fetchPublishedShouts = async (offset?: number, _client?: Client) => { - const shoutsLoader = loadShouts({ filters: { featured: undefined }, limit: SHOUTS_PER_PAGE, offset }) +const feedLoader = async (options: Partial, _client?: Client) => { + const shoutsLoader = loadShouts({ ...options, limit: SHOUTS_PER_PAGE } as LoadShoutsOptions) return await shoutsLoader() } export const route = { load: async ({ location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => { const offset: number = Number.parseInt(query.offset, 10) - const result = await fetchPublishedShouts(offset) + const result = await feedLoader({ offset }) return result } } -export default (props: RouteSectionProps) => { - const [searchParams] = useSearchParams() +export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => { + const [searchParams] = useSearchParams() // ?period=month const { t } = useLocalize() - const {setNonFeaturedFeed} = useFeed() - const [offset, setOffset] = createSignal(0) - const loadMore = async () => { - const newOffset = offset() + SHOUTS_PER_PAGE - setOffset(newOffset) + const { setFeed } = useFeed() + + // preload all topics + const { addTopics, sortedTopics } = useTopics() + createEffect(() => { + !sortedTopics() && props.data.topics && addTopics(props.data.topics) + }) + + // load more feed + const loadMoreFeed = async (offset?: number) => { + // /feed/:order: - select order setting + const paramPattern = /^(hot|likes)$/ + const order = + (props.params.order && paramPattern.test(props.params.order) + ? props.params.order === 'hot' + ? 'last_comment' + : props.params.order + : 'created_at') || 'created_at' + const options: LoadShoutsOptions = { limit: SHOUTS_PER_PAGE, - offset: newOffset, - order_by: searchParams?.by + offset, + order_by: order } - if (searchParams?.by === 'after') { - const period = searchParams?.by || 'month' + // ?period=month - time period filter + if (searchParams?.period) { + const period = searchParams?.period || 'month' options.filters = { after: getFromDate(period as FeedPeriod) } } - const result = await fetchPublishedShouts(newOffset) - result && setNonFeaturedFeed(result) - return + + const loaded = await feedLoader(options) + loaded && setFeed((prev: Shout[]) => [...prev, ...loaded]) + return loaded as LoadMoreItems } - const shouts = createAsync(async () => props.data || await loadMore()) + + // preload shouts + const shouts = createAsync(async () => { + if (props.data.shouts) { + setFeed(props.data.shouts) + console.debug('[routes.main] feed preloaded') + return props.data.shouts + } + return (await loadMoreFeed()) as Shout[] + }) return ( ) => { key="feed" desc="Independent media project about culture, science, art and society with horizontal editing" > - + - + diff --git a/src/routes/feed/my/[...mode]/[...order].tsx b/src/routes/feed/my/[...mode]/[...order].tsx new file mode 100644 index 00000000..91cf0e10 --- /dev/null +++ b/src/routes/feed/my/[...mode]/[...order].tsx @@ -0,0 +1,114 @@ +import { RouteSectionProps, useSearchParams } from '@solidjs/router' +import { createEffect } from 'solid-js' +import { AUTHORS_PER_PAGE } from '~/components/Views/AllAuthors/AllAuthors' +import { Feed } from '~/components/Views/Feed' +import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' +import { PageLayout } from '~/components/_shared/PageLayout' +import { useFeed } from '~/context/feed' +import { useGraphQL } from '~/context/graphql' +import { useLocalize } from '~/context/localize' +import { ReactionsProvider } from '~/context/reactions' +import { useTopics } from '~/context/topics' +import { + loadCoauthoredShouts, + loadDiscussedShouts, + loadFollowedShouts, + loadUnratedShouts +} from '~/graphql/api/private' +import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' + +const feeds = { + followed: loadFollowedShouts, + discussed: loadDiscussedShouts, + coauthored: loadCoauthoredShouts, + unrated: loadUnratedShouts +} + +export type FeedPeriod = 'week' | 'month' | 'year' +export type FeedSearchParams = { period?: FeedPeriod } + +const getFromDate = (period: FeedPeriod): number => { + const now = new Date() + let d: Date = now + switch (period) { + case 'week': { + d = new Date(now.setDate(now.getDate() - 7)) + break + } + case 'month': { + d = new Date(now.setMonth(now.getMonth() - 1)) + break + } + case 'year': { + d = new Date(now.setFullYear(now.getFullYear() - 1)) + break + } + } + return Math.floor(d.getTime() / 1000) +} + +// /feed/my/followed/hot + +export default (props: RouteSectionProps<{ shouts: Shout[]; topics: Topic[] }>) => { + const [searchParams] = useSearchParams() // ?period=month + const { t } = useLocalize() + const { setFeed } = useFeed() + // TODO: use const { requireAuthentication } = useSession() + const client = useGraphQL() + + // preload all topics + const { addTopics, sortedTopics } = useTopics() + createEffect(() => { + !sortedTopics() && props.data.topics && addTopics(props.data.topics) + }) + + // load more my feed + const loadMoreMyFeed = async (offset?: number) => { + // /feed/my/:mode: + const paramModePattern = /^(followed|discussed|liked|coauthored|unrated)$/ + const mode = + props.params.mode && paramModePattern.test(props.params.mode) ? props.params.mode : 'followed' + const gqlHandler = feeds[mode as keyof typeof feeds] + + // /feed/my/:mode:/:order: - select order setting + const paramOrderPattern = /^(hot|likes)$/ + const order = + (paramOrderPattern.test(props.params.order) + ? props.params.order === 'hot' + ? 'last_comment' + : props.params.order + : 'created_at') || 'created_at' + + const options: LoadShoutsOptions = { + limit: 20, + offset, + order_by: order + } + + // ?period=month - time period filter + if (searchParams?.period) { + const period = searchParams?.period || 'month' + options.filters = { after: getFromDate(period as FeedPeriod) } + } + + const shoutsLoader = gqlHandler(client, options) + const loaded = await shoutsLoader() + loaded && setFeed((prev: Shout[]) => [...prev, ...loaded]) + return loaded as LoadMoreItems + } + + return ( + + + + + + + + ) +}