Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make sidebar sticky #330

Merged
merged 2 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions src/components/main/PageWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { summarize } from '@subsocial/utils/summarize'
import clsx from 'clsx'
import { CID } from 'ipfs-http-client'
import Head from 'next/head'
import React, { ComponentProps, FC } from 'react'
import React, { ComponentProps, FC, useRef } from 'react'
import config from 'src/config'
import { useStickyElement } from 'src/hooks/useStickyElement'
import { resolveIpfsUrl } from 'src/ipfs'
import SideMenu from 'src/layout/SideMenu'
import CreatorDashboardSidebar, {
Expand Down Expand Up @@ -219,16 +220,42 @@ export const PageContent: FC<Props> = ({
</section>
{rightPanel}
{rightPanel === undefined && creatorDashboardSidebarType && (
<div {...sidebarStyles()}>
<CreatorDashboardSidebar dashboardType={creatorDashboardSidebarType} />
<div className='mt-3'>
<TopUsersCard />
</div>
{/* <OnBoardingSidebar hideOnBoardingSidebar={() => setShowOnBoardingSidebar(false)} /> */}
</div>
<CreatorSidebar
{...sidebarStyles()}
creatorDashboardSidebarType={creatorDashboardSidebarType}
/>
)}
</div>
)}
</>
)
}

function CreatorSidebar({
creatorDashboardSidebarType,
...props
}: ComponentProps<'div'> & { creatorDashboardSidebarType: CreatorDashboardSidebarType }) {
const ref = useRef<HTMLDivElement | null>(null)
const { position, top: _top } = useStickyElement({ elRef: ref, top: 76 - BOX_SHADOW_OFFSET })

return (
<div
{...props}
style={{
...props.style,
top: _top,
position,
height: 'fit-content',
maxHeight: 'none',
overflow: 'visible',
}}
ref={ref}
>
<CreatorDashboardSidebar dashboardType={creatorDashboardSidebarType} />
<div className='mt-3'>
<TopUsersCard />
</div>
{/* <OnBoardingSidebar hideOnBoardingSidebar={() => setShowOnBoardingSidebar(false)} /> */}
</div>
)
}
1 change: 1 addition & 0 deletions src/components/profiles/ViewProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ const ProfilePage: NextPage<Props> = props => {
canonical: accountUrl({ address }),
}}
withSidebar
creatorDashboardSidebarType={{ name: 'home-page', variant: 'posts' }}
>
<Component {...props} />
{!shouldHideContent && <AccountActivity address={address.toString()} />}
Expand Down
104 changes: 104 additions & 0 deletions src/hooks/useStickyElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React, { RefObject, useLayoutEffect, useState } from 'react'

export type DimensionProps = {
top?: number
}

type HookProps = {
elRef: RefObject<HTMLElement | null>
} & DimensionProps

type Position = React.CSSProperties['position']

const ceil = (num: number) => Math.ceil(num)

export const useStickyElement = ({
elRef,
top: topPositionAtWhichElementBecomesStickyFromTop = 0,
}: HookProps): {
top: number
position: Position
} => {
const [top, setTop] = useState(0)
const [position, setPosition] = useState<Position>('relative')

useLayoutEffect(() => {
const element = elRef.current
let prevScrollTop = window.scrollY
if (!element) return
const offset = ceil(element.offsetTop)
const handleScroll = () => {
const elementBoundingRect = element.getBoundingClientRect()
const elementBoundingRectTop = ceil(elementBoundingRect.top)
const elementBoundingRectBottom = ceil(elementBoundingRect.bottom)
const topDistanceOfElementRelativeToPageTop = ceil(element.offsetTop)
const topDistanceAtWhichElementBecomesStickyFromBottom = elementBoundingRectTop

const scrollYOffset = window.scrollY
const isScrollingUp = scrollYOffset < prevScrollTop
const isScrollingDown = scrollYOffset > prevScrollTop
const windowHeight = document.documentElement.clientHeight

const topEndPosition = elementBoundingRectTop - topPositionAtWhichElementBecomesStickyFromTop
const bottomEndPosition = elementBoundingRectBottom

const isTopEndAboveViewport = topEndPosition < 0
const isTopEndBelowViewport = topEndPosition > windowHeight
const isBottomEndBelowViewport = bottomEndPosition > windowHeight
const isBottomEndAboveViewport = bottomEndPosition < 0

const isTopEndBetweenViewport = !isTopEndAboveViewport && !isTopEndBelowViewport
const isBottomEndBetweenViewport = !isBottomEndAboveViewport && !isBottomEndBelowViewport

const areBothTopAndBottomEndsOnOppositeEndsOfViewport =
isTopEndAboveViewport && isBottomEndBelowViewport
const areBothTopAndBottomEndsBetweenViewport =
isTopEndBetweenViewport && isBottomEndBetweenViewport

if (isTopEndBelowViewport || isBottomEndAboveViewport) {
setPosition('relative')
setTop(scrollYOffset)
prevScrollTop = scrollYOffset
return
}
if (areBothTopAndBottomEndsOnOppositeEndsOfViewport) {
setPosition('relative')
setTop(topDistanceOfElementRelativeToPageTop - offset)
prevScrollTop = scrollYOffset
return
}

if (areBothTopAndBottomEndsBetweenViewport) {
setPosition('sticky')
setTop(topPositionAtWhichElementBecomesStickyFromTop)
prevScrollTop = scrollYOffset
return
}

if (isScrollingUp) {
if (isTopEndBetweenViewport) {
setPosition('sticky')
setTop(topPositionAtWhichElementBecomesStickyFromTop)
} else if (isBottomEndBetweenViewport) {
setPosition('relative')
setTop(topDistanceOfElementRelativeToPageTop - offset)
}
} else if (isScrollingDown) {
if (isTopEndBetweenViewport) {
setPosition('relative')
setTop(topDistanceOfElementRelativeToPageTop - offset)
} else if (isBottomEndBetweenViewport) {
setPosition('sticky')
setTop(topDistanceAtWhichElementBecomesStickyFromBottom)
}
}
prevScrollTop = scrollYOffset
}
window.addEventListener('scroll', handleScroll, { passive: true })
return () => {
window.removeEventListener('scroll', handleScroll)
}
}, [])

return { top, position }
}
Loading