From edbd7ec3b2848fb531083733f7f0b5d883a91f46 Mon Sep 17 00:00:00 2001 From: Untone Date: Tue, 9 Jul 2024 20:41:14 +0300 Subject: [PATCH] :topic-page-fix --- src/components/Topic/Full.tsx | 19 +- src/components/Views/Author/Author.tsx | 18 +- src/components/Views/FourOuFour.tsx | 1 + src/components/Views/Topic.tsx | 281 +++++++++++++------------ src/graphql/api/public.ts | 10 + src/routes/author/[slug]/[tab].tsx | 47 ++++- src/routes/topic/[slug].tsx | 58 ++++- 7 files changed, 258 insertions(+), 176 deletions(-) diff --git a/src/components/Topic/Full.tsx b/src/components/Topic/Full.tsx index 09baba15..874f45d5 100644 --- a/src/components/Topic/Full.tsx +++ b/src/components/Topic/Full.tsx @@ -26,16 +26,15 @@ export const FullTopic = (props: Props) => { const { requireAuthentication } = useSession() const [followed, setFollowed] = createSignal() - const title = createMemo( - () => - // FIXME: use title translation - `#${capitalize( - lang() === 'en' - ? props.topic.slug.replace(/-/, ' ') - : props.topic.title || props.topic.slug.replace(/-/, ' '), - true - )}` - ) + const title = createMemo(() => { + /* FIXME: use title translation*/ + return `#${capitalize( + lang() === 'en' + ? props.topic.slug.replace(/-/, ' ') + : props.topic.title || props.topic.slug.replace(/-/, ' '), + true + )}` + }) createEffect(() => { if (follows?.topics?.length !== 0) { const items = follows.topics || [] diff --git a/src/components/Views/Author/Author.tsx b/src/components/Views/Author/Author.tsx index 3f824a44..9e1bb61e 100644 --- a/src/components/Views/Author/Author.tsx +++ b/src/components/Views/Author/Author.tsx @@ -8,7 +8,6 @@ import { useFollowing } from '~/context/following' import { useGraphQL } from '~/context/graphql' import { useLocalize } from '~/context/localize' import { useSession } from '~/context/session' -import { useUI } from '~/context/ui' import { loadReactions } from '~/graphql/api/public' import loadShoutsQuery from '~/graphql/query/core/articles-load-by' import getAuthorFollowersQuery from '~/graphql/query/core/author-followers' @@ -31,6 +30,7 @@ type Props = { authorSlug: string shouts?: Shout[] author?: Author + topics?: Topic[] selectedTab: string } @@ -38,6 +38,7 @@ export const PRERENDERED_ARTICLES_COUNT = 12 const LOAD_MORE_PAGE_SIZE = 9 export const AuthorView = (props: Props) => { + console.debug('[components.AuthorView] reactive context init...') const { t } = useLocalize() const params = useParams() const { followers: myFollowers, follows: myFollows } = useFollowing() @@ -45,7 +46,6 @@ export const AuthorView = (props: Props) => { const me = createMemo(() => session()?.user?.app_data?.profile as Author) const [authorSlug, setSlug] = createSignal(props.authorSlug) const { sortedFeed } = useFeed() - const { modal, hideModal } = useUI() const loc = useLocation() const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false) @@ -90,10 +90,11 @@ export const AuthorView = (props: Props) => { } }) ) - // 3 // after fetch loading following data + + // 2 // догружает подписки автора createEffect( on( - [followers, () => authorsEntities()[authorSlug()]], + [followers, () => props.author || authorsEntities()[authorSlug()]], async ([current, found]) => { if (current) return if (!found) return @@ -112,7 +113,7 @@ export const AuthorView = (props: Props) => { ) ) - // догружает ленту и комментарии + // 3 // догружает ленту и комментарии createEffect( on( () => author() as Author, @@ -139,11 +140,6 @@ export const AuthorView = (props: Props) => { } } - onMount(() => { - if (!modal()) hideModal() - checkBioHeight() - }) - const pages = createMemo(() => splitToPages(sortedFeed(), PRERENDERED_ARTICLES_COUNT, LOAD_MORE_PAGE_SIZE) ) @@ -151,6 +147,8 @@ export const AuthorView = (props: Props) => { setCommented((prev) => (prev || []).filter((comment) => comment.id !== id)) } + onMount(checkBioHeight) + return (
diff --git a/src/components/Views/FourOuFour.tsx b/src/components/Views/FourOuFour.tsx index d9681289..dc070ec2 100644 --- a/src/components/Views/FourOuFour.tsx +++ b/src/components/Views/FourOuFour.tsx @@ -9,6 +9,7 @@ import styles from '../../styles/FourOuFour.module.scss' type EvType = Event & { submitter: HTMLElement } & { currentTarget: HTMLFormElement; target: Element } export const FourOuFourView = () => { + console.debug('[components.404] init context...') let queryInput: HTMLInputElement | null const navigate = useNavigate() const search = (_ev: EvType) => navigate(`/search?q=${queryInput?.value || ''}`) diff --git a/src/components/Views/Topic.tsx b/src/components/Views/Topic.tsx index 3bc4bba2..c5c43901 100644 --- a/src/components/Views/Topic.tsx +++ b/src/components/Views/Topic.tsx @@ -1,16 +1,13 @@ import { useSearchParams } from '@solidjs/router' import { clsx } from 'clsx' -import { For, Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' +import { For, Show, Suspense, createEffect, createMemo, createSignal, on, onMount } from 'solid-js' import { useAuthors } from '~/context/authors' import { useFeed } from '~/context/feed' -import { useGraphQL } from '~/context/graphql' import { useLocalize } from '~/context/localize' import { useTopics } from '~/context/topics' -import getRandomTopShoutsQuery from '~/graphql/query/core/articles-load-random-top' -import loadShoutsRandomQuery from '~/graphql/query/core/articles-load-random-topic' -import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by' -import getTopicFollowersQuery from '~/graphql/query/core/topic-followers' +import { loadAuthors, loadFollowersByTopic, loadShouts } from '~/graphql/api/public' import { Author, AuthorsBy, LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' +import { SHOUTS_PER_PAGE } from '~/routes/(home)' import { getUnixtime } from '~/utils/getServerDate' import { restoreScrollPosition, saveScrollPosition } from '~/utils/scroll' import { splitToPages } from '~/utils/splitToPages' @@ -20,6 +17,7 @@ import { Row1 } from '../Feed/Row1' import { Row2 } from '../Feed/Row2' import { Row3 } from '../Feed/Row3' import { FullTopic } from '../Topic/Full' +import { Loading } from '../_shared/Loading' import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper' type TopicsPageSearchParams = { @@ -38,87 +36,96 @@ const LOAD_MORE_PAGE_SIZE = 9 // Row3 + Row3 + Row3 export const TopicView = (props: Props) => { const { t } = useLocalize() - const { query } = useGraphQL() - const [searchParams, changeSearchParams] = useSearchParams() - const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) - const { feedByTopic, loadShouts } = useFeed() - const sortedFeed = createMemo(() => feedByTopic()[topic()?.slug || ''] || []) + const { feedByTopic, addFeed } = useFeed() const { topicEntities } = useTopics() const { authorsByTopic } = useAuthors() + const [searchParams, changeSearchParams] = useSearchParams() + const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [favoriteTopArticles, setFavoriteTopArticles] = createSignal([]) const [reactedTopMonthArticles, setReactedTopMonthArticles] = createSignal([]) const [topic, setTopic] = createSignal() - createEffect( - on([() => props.topicSlug, topic, topicEntities], async ([slug, t, ttt]) => { - if (slug && !t && ttt) { - const current = slug in ttt ? ttt[slug] : null - console.debug(current) - setTopic(current as Topic) - await loadTopicFollowers() - await loadTopicAuthors() - loadRandom() - } - }) - ) - const [followers, setFollowers] = createSignal(props.followers || []) + const sortedFeed = createMemo(() => feedByTopic()[topic()?.slug || ''] || []) // TODO: filter + sort + const loadTopicFollowers = async () => { - const resp = await query(getTopicFollowersQuery, { slug: props.topicSlug }).toPromise() - setFollowers(resp?.data?.get_topic_followers || []) + const topicFollowersFetcher = loadFollowersByTopic(props.topicSlug) + const topicFollowers = await topicFollowersFetcher() + topicFollowers && setFollowers(topicFollowers) } + const [topicAuthors, setTopicAuthors] = createSignal([]) const loadTopicAuthors = async () => { const by: AuthorsBy = { topic: props.topicSlug } - const resp = await query(loadAuthorsByQuery, { by, limit: 10, offset: 0 }).toPromise() - setTopicAuthors(resp?.data?.load_authors_by || []) + const topicAuthorsFetcher = await loadAuthors({ by, limit: 10, offset: 0 }) + const result = await topicAuthorsFetcher() + result && setTopicAuthors(result) } - const loadFavoriteTopArticles = async (topic: string) => { + const loadFavoriteTopArticles = async () => { const options: LoadShoutsOptions = { - filters: { featured: true, topic: topic }, + filters: { featured: true, topic: props.topicSlug }, limit: 10, random_limit: 100 } - const resp = await query(getRandomTopShoutsQuery, { options }).toPromise() - setFavoriteTopArticles(resp?.data?.l) + const topicRandomShoutsFetcher = loadShouts(options) + const result = await topicRandomShoutsFetcher() + result && setFavoriteTopArticles(result) } - const loadReactedTopMonthArticles = async (topic: string) => { + const loadReactedTopMonthArticles = async () => { const now = new Date() const after = getUnixtime(new Date(now.setMonth(now.getMonth() - 1))) const options: LoadShoutsOptions = { - filters: { after: after, featured: true, topic: topic }, + filters: { after: after, featured: true, topic: props.topicSlug }, limit: 10, random_limit: 10 } - const resp = await query(loadShoutsRandomQuery, { options }).toPromise() - setReactedTopMonthArticles(resp?.data?.load_shouts_random) + const reactedTopMonthShoutsFetcher = loadShouts(options) + const result = await reactedTopMonthShoutsFetcher() + result && setReactedTopMonthArticles(result) } - const loadRandom = () => { - if (topic()) { - loadFavoriteTopArticles((topic() as Topic).slug) - loadReactedTopMonthArticles((topic() as Topic).slug) - } - } + // второй этап начальной загрузки данных + createEffect( + on( + topicEntities, + (ttt: Record) => { + if (props.topicSlug in ttt) { + Promise.all([ + loadFavoriteTopArticles(), + loadReactedTopMonthArticles(), + loadTopicAuthors(), + loadTopicFollowers() + ]).finally(() => { + setTopic(ttt[props.topicSlug]) + }) + } + }, + { defer: true } + ) + ) + // дозагрузка const loadMore = async () => { saveScrollPosition() - - const { hasMore } = await loadShouts({ - filters: { topic: topic()?.slug }, - limit: LOAD_MORE_PAGE_SIZE, - offset: sortedFeed().length // FIXME: use feedByTopic + const amountBefore = feedByTopic()?.[props.topicSlug]?.length || 0 + const topicShoutsFetcher = loadShouts({ + filters: { topic: props.topicSlug }, + limit: SHOUTS_PER_PAGE, + offset: amountBefore }) - setIsLoadMoreButtonVisible(hasMore) - + const result = await topicShoutsFetcher() + if (result) { + addFeed(result) + const amountAfter = feedByTopic()[props.topicSlug].length + setIsLoadMoreButtonVisible(amountBefore !== amountAfter) + } restoreScrollPosition() } onMount(() => { - loadRandom() if (sortedFeed() || [].length === PRERENDERED_ARTICLES_COUNT) { loadMore() } @@ -137,101 +144,105 @@ export const TopicView = (props: Props) => { ) return (
- -
-
-
-
    -
  • - -
  • - {/*TODO: server sort*/} - {/*
  • */} - {/* */} - {/*
  • */} - {/*
  • */} - {/* */} - {/*
  • */} - {/*
  • */} - {/* */} - {/*
  • */} -
-
-
-
- {`${t('Show')} `} - {t('All posts')} + + + {/*TODO: server sort*/} + {/*
  • */} + {/* */} + {/*
  • */} + {/*
  • */} + {/* */} + {/*
  • */} + {/*
  • */} + {/* */} + {/*
  • */} + +
    +
    +
    + {`${t('Show')} `} + {t('All posts')} +
    -
    - - + + - - 0} keyed={true}> - - - + + 0} keyed={true}> + + + - - + + - 0} keyed={true}> - - - 15}> - - - + 0} keyed={true}> + + + 15}> + + + - - {(page) => ( - <> - - - - - )} - + + {(page) => ( + <> + + + + + )} + - -

    - -

    -
    + +

    + +

    +
    +
    ) } diff --git a/src/graphql/api/public.ts b/src/graphql/api/public.ts index 5ef2049b..4a482794 100644 --- a/src/graphql/api/public.ts +++ b/src/graphql/api/public.ts @@ -7,6 +7,7 @@ import getAuthorQuery from '~/graphql/query/core/author-by' import loadAuthorsAllQuery from '~/graphql/query/core/authors-all' import loadAuthorsByQuery from '~/graphql/query/core/authors-load-by' import loadReactionsByQuery from '~/graphql/query/core/reactions-load-by' +import loadFollowersByTopicQuery from '~/graphql/query/core/topic-followers' import loadTopicsQuery from '~/graphql/query/core/topics-all' import { Author, @@ -100,3 +101,12 @@ export const loadShoutsSearch = (options: QueryLoad_Shouts_SearchArgs) => { if (result) return result as Shout[] }, `search-${options.text}-${page}`) } + +export const loadFollowersByTopic = (slug: string) => { + // TODO: paginate topic followers + return cache(async () => { + const resp = await defaultClient.query(loadFollowersByTopicQuery, { slug }).toPromise() + const result = resp?.data?.load_authors_by + if (result) return result as Author[] + }, `topic-${slug}`) +} diff --git a/src/routes/author/[slug]/[tab].tsx b/src/routes/author/[slug]/[tab].tsx index fb150d0f..3d0e74c8 100644 --- a/src/routes/author/[slug]/[tab].tsx +++ b/src/routes/author/[slug]/[tab].tsx @@ -1,5 +1,5 @@ import { RouteSectionProps, createAsync, useParams } from '@solidjs/router' -import { ErrorBoundary, Suspense, createMemo, createReaction } from 'solid-js' +import { ErrorBoundary, Suspense, createEffect, createMemo } from 'solid-js' import { AuthorView } from '~/components/Views/Author' import { FourOuFourView } from '~/components/Views/FourOuFour' import { Loading } from '~/components/_shared/Loading' @@ -7,8 +7,14 @@ import { PageLayout } from '~/components/_shared/PageLayout' import { useAuthors } from '~/context/authors' import { useLocalize } from '~/context/localize' import { ReactionsProvider } from '~/context/reactions' -import { loadShouts } from '~/graphql/api/public' -import { Author, LoadShoutsOptions, Shout } from '~/graphql/schema/core.gen' +import { loadAuthors, loadShouts, loadTopics } from '~/graphql/api/public' +import { + Author, + LoadShoutsOptions, + QueryLoad_Authors_ByArgs, + Shout, + Topic +} from '~/graphql/schema/core.gen' import { getImageUrl } from '~/lib/getImageUrl' import { SHOUTS_PER_PAGE } from '../../(home)' @@ -18,29 +24,49 @@ const fetchAuthorShouts = async (slug: string, offset?: number) => { return await shoutsLoader() } +const fetchAllTopics = async () => { + const topicsFetcher = loadTopics() + return await topicsFetcher() +} + +const fetchAuthor = async (slug: string) => { + const authorFetcher = loadAuthors({ by: { slug }, limit: 1 } as QueryLoad_Authors_ByArgs) + const aaa = await authorFetcher() + return aaa?.[0] +} + export const route = { load: async ({ params, location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => { const offset: number = Number.parseInt(query.offset, 10) const result = await fetchAuthorShouts(params.slug, offset) - return result + return { + author: await fetchAuthor(params.slug), + shouts: result || [], + topics: await fetchAllTopics() + } } } -export default (props: RouteSectionProps<{ articles: Shout[] }>) => { +export default (props: RouteSectionProps<{ articles: Shout[]; author: Author; topics: Topic[] }>) => { const params = useParams() + const { addAuthor } = useAuthors() const articles = createAsync( async () => props.data.articles || (await fetchAuthorShouts(params.slug)) || [] ) - const { authorsEntities } = useAuthors() + const author = createAsync(async () => { + const a = props.data.author || (await fetchAuthor(params.slug)) + addAuthor(a) + return a + }) + const topics = createAsync(async () => props.data.topics || (await fetchAllTopics())) const { t } = useLocalize() - const author = createMemo(() => authorsEntities?.()[params.slug]) const title = createMemo(() => `${author()?.name || ''}`) // docs: `a side effect that is run the first time the expression // wrapped by the returned tracking function is notified of a change` - createReaction(() => { + createEffect(() => { if (author()) { - console.debug('[routes.slug] article signal changed once') + console.debug('[routes] author/[slug] author loaded fx') window?.gtag?.('event', 'page_view', { page_title: author()?.name || '', page_location: window?.location.href || '', @@ -69,7 +95,8 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => { author={author() as Author} authorSlug={params.slug} shouts={articles() as Shout[]} - selectedTab={params.tab || ''} + selectedTab={params.tab || 'shouts'} + topics={topics()} /> diff --git a/src/routes/topic/[slug].tsx b/src/routes/topic/[slug].tsx index 35ffb375..9de1e41c 100644 --- a/src/routes/topic/[slug].tsx +++ b/src/routes/topic/[slug].tsx @@ -1,12 +1,13 @@ import { RouteSectionProps, createAsync, useParams } from '@solidjs/router' -import { ErrorBoundary, Suspense, createEffect, createMemo } from 'solid-js' +import { HttpStatusCode } from '@solidjs/start' +import { Show, Suspense, createEffect, createMemo, createSignal } from 'solid-js' import { FourOuFourView } from '~/components/Views/FourOuFour' import { TopicView } from '~/components/Views/Topic' import { Loading } from '~/components/_shared/Loading' import { PageLayout } from '~/components/_shared/PageLayout' import { useLocalize } from '~/context/localize' import { useTopics } from '~/context/topics' -import { loadShouts } from '~/graphql/api/public' +import { loadShouts, loadTopics } from '~/graphql/api/public' import { LoadShoutsOptions, Shout, Topic } from '~/graphql/schema/core.gen' import { getImageUrl } from '~/lib/getImageUrl' import { getArticleDescription } from '~/utils/meta' @@ -18,23 +19,47 @@ const fetchTopicShouts = async (slug: string, offset?: number) => { return await shoutsLoader() } +const fetchAllTopics = async () => { + const topicsFetcher = loadTopics() + return await topicsFetcher() +} + export const route = { load: async ({ params, location: { query } }: RouteSectionProps<{ articles: Shout[] }>) => { const offset: number = Number.parseInt(query.offset, 10) const result = await fetchTopicShouts(params.slug, offset) - return result + return { + articles: result, + topics: await fetchAllTopics() + } } } -export default (props: RouteSectionProps<{ articles: Shout[] }>) => { +export default (props: RouteSectionProps<{ articles: Shout[]; topics: Topic[] }>) => { + const { t } = useLocalize() const params = useParams() + const { addTopics } = useTopics() + const [loadingError, setLoadingError] = createSignal(false) + + const topic = createAsync(async () => { + try { + const ttt: Topic[] = props.data.topics || (await fetchAllTopics()) || [] + addTopics(ttt) + console.debug('[route.topic] all topics loaded') + const t = ttt.find((x) => x.slug === params.slug) + return t + } catch (_error) { + setLoadingError(true) + return null + } + }) + const articles = createAsync( async () => props.data.articles || (await fetchTopicShouts(params.slug)) || [] ) - const { topicEntities } = useTopics() - const { t } = useLocalize() - const topic = createMemo(() => topicEntities?.()[params.slug]) + const title = createMemo(() => `${t('Discours')} :: ${topic()?.title || ''}`) + createEffect(() => { if (topic() && window) { window?.gtag?.('event', 'page_view', { @@ -44,19 +69,30 @@ export default (props: RouteSectionProps<{ articles: Shout[] }>) => { }) } }) + const desc = createMemo(() => topic()?.body ? getArticleDescription(topic()?.body || '') : t('The most interesting publications on the topic', { topicName: title() }) ) + const cover = createMemo(() => topic()?.pic ? getImageUrl(topic()?.pic || '', { width: 1200 }) : getImageUrl('production/image/logo_image.png') ) + return ( - }> - }> + }> + + + + + } + > ) => { shouts={articles() as Shout[]} /> - - + + ) }