Skip to content

Commit

Permalink
feat(Interaction): implement DualScroll component for Collection view
Browse files Browse the repository at this point in the history
  • Loading branch information
Kechicode committed Dec 16, 2024
1 parent a2a5b3b commit fad39ae
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 44 deletions.
36 changes: 36 additions & 0 deletions src/components/Interaction/DualScroll/EndOfResults/index.tsx
Original file line number Diff line number Diff line change
@@ -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<EndOfResultsProps> = ({
message,
spacingTop = 'xLoose',
}) => {
const containerClasses = classNames({
[styles.endOfResults]: true,
[styles[`spacingTop${capitalizeFirstLetter(spacingTop)}`]]: true,
})
return (
<section className={containerClasses}>
{typeof message === 'boolean' && message ? (
<FormattedMessage
defaultMessage="That's all"
id="B2As08"
description="src/components/Interaction/InfiniteScroll/EndOfResults/index.tsx"
/>
) : (
message
)}
</section>
)
}

export default EndOfResults
Original file line number Diff line number Diff line change
@@ -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);
}
74 changes: 74 additions & 0 deletions src/components/Interaction/DualScroll/index.tsx
Original file line number Diff line number Diff line change
@@ -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<any>
loadPrevious: () => Promise<any>
loader?: React.ReactNode
eof?: React.ReactNode
eofSpacingTop?: 'base' | 'xLoose'
scrollableAncestor?: any
className?: string
}

export const DualScroll: React.FC<React.PropsWithChildren<Props>> = ({
hasNextPage,
hasPreviousPage,
loader = <SpinnerBlock />,
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<HTMLDivElement>) => {
const element = event.currentTarget
if (element.scrollTop === 0 && hasPreviousPage) {
setIsPrevLoading(true)
await loadPrevious()
setIsPrevLoading(false)
}
}

return (
<div
onScroll={handleScroll}
style={{ overflow: 'auto' }}
className={className}
>
{/* Top loader */}
{hasPreviousPage && isPrevLoading ? loader : null}

{/* Main content */}
{children}

{/* Bottom loader */}
{hasNextPage && <span ref={bottomObserver.ref} />}
{hasNextPage && loader}

{!hasNextPage && eof && (
<EndOfResults message={eof} spacingTop={eofSpacingTop} />
)}
</div>
)
}
1 change: 1 addition & 0 deletions src/components/Interaction/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './Card'
export * from './DualScroll'
export * from './InfiniteScroll'
export * from './LinkWrapper'
74 changes: 30 additions & 44 deletions src/views/ArticleDetail/AuthorSidebar/Collection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '~/common/utils'
import {
ArticleDigestAuthorSidebar,
InfiniteScroll,
DualScroll,
LinkWrapper,
List,
QueryError,
Expand Down Expand Up @@ -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<string | null>(null)

/**
Expand Down Expand Up @@ -128,7 +127,6 @@ const Collection = ({ article, collectionId }: CollectionProps) => {
path: connectionPath,
}),
})
setIsPrevLoading(false)

const lastTopArticleDigest = document.getElementById(
`${ARTICLE_DIGEST_AUTHOR_SIDEBAR_ID_PREFIX}${lastTopArticleId}`
Expand Down Expand Up @@ -185,49 +183,37 @@ const Collection = ({ article, collectionId }: CollectionProps) => {
</section>
</LinkWrapper>
)}
<section
<DualScroll
hasPreviousPage={prevPageInfo?.hasPreviousPage}
hasNextPage={afterPageInfo?.hasNextPage}
loadPrevious={loadPreviousMore}
loadMore={loadAfterMore}
loader={<ArticleDigestAuthorSidebarFeedPlaceholder />}
className={styles.feed}
onScroll={(event) => {
const element = event.currentTarget
if (element.scrollTop === 0) {
if (!prevPageInfo?.hasPreviousPage) {
return
}
setIsPrevLoading(true)
loadPreviousMore()
}
}}
>
{isPrevLoading && <ArticleDigestAuthorSidebarFeedPlaceholder />}
<InfiniteScroll
hasNextPage={afterPageInfo?.hasNextPage}
loadMore={loadAfterMore}
loader={<ArticleDigestAuthorSidebarFeedPlaceholder />}
>
<List borderPosition="top">
{edges?.map(({ node, cursor }, i) => (
<List.Item key={cursor}>
<ArticleDigestAuthorSidebar
article={node}
titleTextSize={14}
collectionId={collectionId}
cursor={cursor}
titleColor={node.id === article?.id ? 'black' : 'greyDarker'}
showCover={false}
clickEvent={() => {
analytics.trackEvent('click_feed', {
type: 'article_detail_author_sidebar_collection',
contentType: 'article',
location: i,
id: node.id,
})
}}
/>
</List.Item>
))}
</List>
</InfiniteScroll>
</section>
<List borderPosition="top">
{edges?.map(({ node, cursor }, i) => (
<List.Item key={cursor}>
<ArticleDigestAuthorSidebar
article={node}
titleTextSize={14}
collectionId={collectionId}
cursor={cursor}
titleColor={node.id === article?.id ? 'black' : 'greyDarker'}
showCover={false}
clickEvent={() => {
analytics.trackEvent('click_feed', {
type: 'article_detail_author_sidebar_collection',
contentType: 'article',
location: i,
id: node.id,
})
}}
/>
</List.Item>
))}
</List>
</DualScroll>
</section>
)
}
Expand Down

0 comments on commit fad39ae

Please sign in to comment.