From fad39aeddc66be0ea21a73e795230a0703a6654a Mon Sep 17 00:00:00 2001 From: Kechicode <186776112+Kechicode@users.noreply.github.com> Date: Mon, 16 Dec 2024 15:55:55 +0800 Subject: [PATCH] feat(Interaction): implement DualScroll component for Collection view --- .../DualScroll/EndOfResults/index.tsx | 36 +++++++++ .../DualScroll/EndOfResults/styles.module.css | 15 ++++ .../Interaction/DualScroll/index.tsx | 74 +++++++++++++++++++ src/components/Interaction/index.ts | 1 + .../AuthorSidebar/Collection/index.tsx | 74 ++++++++----------- 5 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 src/components/Interaction/DualScroll/EndOfResults/index.tsx create mode 100644 src/components/Interaction/DualScroll/EndOfResults/styles.module.css create mode 100644 src/components/Interaction/DualScroll/index.tsx diff --git a/src/components/Interaction/DualScroll/EndOfResults/index.tsx b/src/components/Interaction/DualScroll/EndOfResults/index.tsx new file mode 100644 index 0000000000..9c38971a37 --- /dev/null +++ b/src/components/Interaction/DualScroll/EndOfResults/index.tsx @@ -0,0 +1,36 @@ +import classNames from 'classnames' +import { FormattedMessage } from 'react-intl' + +import { capitalizeFirstLetter } from '~/common/utils' + +import styles from './styles.module.css' + +type EndOfResultsProps = { + message?: React.ReactNode + spacingTop?: 'base' | 'xLoose' +} + +const EndOfResults: React.FC = ({ + message, + spacingTop = 'xLoose', +}) => { + const containerClasses = classNames({ + [styles.endOfResults]: true, + [styles[`spacingTop${capitalizeFirstLetter(spacingTop)}`]]: true, + }) + return ( +
+ {typeof message === 'boolean' && message ? ( + + ) : ( + message + )} +
+ ) +} + +export default EndOfResults diff --git a/src/components/Interaction/DualScroll/EndOfResults/styles.module.css b/src/components/Interaction/DualScroll/EndOfResults/styles.module.css new file mode 100644 index 0000000000..50d5a90dda --- /dev/null +++ b/src/components/Interaction/DualScroll/EndOfResults/styles.module.css @@ -0,0 +1,15 @@ +.endOfResults { + @mixin flex-center-all; + + padding-bottom: var(--sp40); + font-size: var(--text14); + color: var(--color-grey); +} + +.spacingTopBase { + padding-top: var(--sp16); +} + +.spacingTopXLoose { + padding-top: var(--sp32); +} diff --git a/src/components/Interaction/DualScroll/index.tsx b/src/components/Interaction/DualScroll/index.tsx new file mode 100644 index 0000000000..3749ba73e6 --- /dev/null +++ b/src/components/Interaction/DualScroll/index.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react' + +import { SpinnerBlock, useIntersectionObserver } from '~/components' + +import EndOfResults from './EndOfResults' + +interface Props { + hasNextPage: boolean + hasPreviousPage: boolean + loadMore: () => Promise + loadPrevious: () => Promise + loader?: React.ReactNode + eof?: React.ReactNode + eofSpacingTop?: 'base' | 'xLoose' + scrollableAncestor?: any + className?: string +} + +export const DualScroll: React.FC> = ({ + hasNextPage, + hasPreviousPage, + loader = , + loadMore, + loadPrevious, + eof, + eofSpacingTop, + scrollableAncestor, + children, + className, +}) => { + const [isPrevLoading, setIsPrevLoading] = useState(false) + + // Only use intersection observer for bottom loading + const bottomObserver = useIntersectionObserver() + + // Handle bottom intersection + useEffect(() => { + if (bottomObserver.isIntersecting && hasNextPage) { + loadMore() + } + }, [bottomObserver.isIntersecting]) + + // Handle scroll for top loading + const handleScroll = async (event: React.UIEvent) => { + const element = event.currentTarget + if (element.scrollTop === 0 && hasPreviousPage) { + setIsPrevLoading(true) + await loadPrevious() + setIsPrevLoading(false) + } + } + + return ( +
+ {/* Top loader */} + {hasPreviousPage && isPrevLoading ? loader : null} + + {/* Main content */} + {children} + + {/* Bottom loader */} + {hasNextPage && } + {hasNextPage && loader} + + {!hasNextPage && eof && ( + + )} +
+ ) +} diff --git a/src/components/Interaction/index.ts b/src/components/Interaction/index.ts index 0ab9ab9a9d..5ef5ce4761 100644 --- a/src/components/Interaction/index.ts +++ b/src/components/Interaction/index.ts @@ -1,3 +1,4 @@ export * from './Card' +export * from './DualScroll' export * from './InfiniteScroll' export * from './LinkWrapper' diff --git a/src/views/ArticleDetail/AuthorSidebar/Collection/index.tsx b/src/views/ArticleDetail/AuthorSidebar/Collection/index.tsx index ec7d2778ea..59f146afd0 100644 --- a/src/views/ArticleDetail/AuthorSidebar/Collection/index.tsx +++ b/src/views/ArticleDetail/AuthorSidebar/Collection/index.tsx @@ -10,7 +10,7 @@ import { } from '~/common/utils' import { ArticleDigestAuthorSidebar, - InfiniteScroll, + DualScroll, LinkWrapper, List, QueryError, @@ -38,7 +38,6 @@ type CollectionProps = { const Collection = ({ article, collectionId }: CollectionProps) => { const { getQuery } = useRoute() const cursor = getQuery('cursor') - const [isPrevLoading, setIsPrevLoading] = useState(false) const [lastTopArticleId, setLastTopArticleId] = useState(null) /** @@ -128,7 +127,6 @@ const Collection = ({ article, collectionId }: CollectionProps) => { path: connectionPath, }), }) - setIsPrevLoading(false) const lastTopArticleDigest = document.getElementById( `${ARTICLE_DIGEST_AUTHOR_SIDEBAR_ID_PREFIX}${lastTopArticleId}` @@ -185,49 +183,37 @@ const Collection = ({ article, collectionId }: CollectionProps) => { )} -
} className={styles.feed} - onScroll={(event) => { - const element = event.currentTarget - if (element.scrollTop === 0) { - if (!prevPageInfo?.hasPreviousPage) { - return - } - setIsPrevLoading(true) - loadPreviousMore() - } - }} > - {isPrevLoading && } - } - > - - {edges?.map(({ node, cursor }, i) => ( - - { - analytics.trackEvent('click_feed', { - type: 'article_detail_author_sidebar_collection', - contentType: 'article', - location: i, - id: node.id, - }) - }} - /> - - ))} - - -
+ + {edges?.map(({ node, cursor }, i) => ( + + { + analytics.trackEvent('click_feed', { + type: 'article_detail_author_sidebar_collection', + contentType: 'article', + location: i, + id: node.id, + }) + }} + /> + + ))} + + ) }