feat: improve mobile experience

This commit is contained in:
codytseng
2025-01-02 21:57:14 +08:00
parent 8ec0d46d58
commit 3946e603b3
98 changed files with 2508 additions and 1058 deletions

View File

@@ -1,70 +1,112 @@
import Logo from '@/assets/Logo'
import AccountButton from '@/components/AccountButton'
import NotificationButton from '@/components/NotificationButton'
import PostButton from '@/components/PostButton'
import RelaySettingsButton from '@/components/RelaySettingsButton'
import BottomNavigationBar from '@/components/BottomNavigationBar'
import ScrollToTopButton from '@/components/ScrollToTopButton'
import SearchButton from '@/components/SearchButton'
import ThemeToggle from '@/components/ThemeToggle'
import { Titlebar } from '@/components/Titlebar'
import { ScrollArea } from '@/components/ui/scroll-area'
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
const PrimaryPageLayout = forwardRef(({ children }: { children?: React.ReactNode }, ref) => {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const [visible, setVisible] = useState(true)
const [lastScrollTop, setLastScrollTop] = useState(0)
const PrimaryPageLayout = forwardRef(
(
{
children,
titlebar,
pageName,
displayScrollToTopButton = false
}: {
children?: React.ReactNode
titlebar?: React.ReactNode
pageName: TPrimaryPageName
displayScrollToTopButton?: boolean
},
ref
) => {
const scrollAreaRef = useRef<HTMLDivElement>(null)
const [visible, setVisible] = useState(true)
const [lastScrollTop, setLastScrollTop] = useState(0)
const { isSmallScreen } = useScreenSize()
const { current } = usePrimaryPage()
useImperativeHandle(
ref,
() => ({
scrollToTop: () => {
scrollAreaRef.current?.scrollTo({ top: 0 })
}
}),
[]
)
useImperativeHandle(
ref,
() => ({
scrollToTop: () => {
if (isSmallScreen) {
window.scrollTo({ top: 0 })
return
}
scrollAreaRef.current?.scrollTo({ top: 0 })
}
}),
[]
)
useEffect(() => {
const handleScroll = () => {
const scrollTop = scrollAreaRef.current?.scrollTop || 0
const diff = scrollTop - lastScrollTop
if (scrollTop <= 100) {
useEffect(() => {
if (isSmallScreen) {
window.scrollTo({ top: 0 })
setVisible(true)
setLastScrollTop(scrollTop)
return
}
}, [current])
if (diff > 20) {
setVisible(false)
setLastScrollTop(scrollTop)
} else if (diff < -20) {
setVisible(true)
setLastScrollTop(scrollTop)
useEffect(() => {
if (current !== pageName) return
const handleScroll = () => {
const scrollTop = (isSmallScreen ? window.scrollY : scrollAreaRef.current?.scrollTop) || 0
const diff = scrollTop - lastScrollTop
if (scrollTop <= 800) {
setVisible(true)
setLastScrollTop(scrollTop)
return
}
if (diff > 20) {
setVisible(false)
setLastScrollTop(scrollTop)
} else if (diff < -20) {
setVisible(true)
setLastScrollTop(scrollTop)
}
}
}
const scrollArea = scrollAreaRef.current
scrollArea?.addEventListener('scroll', handleScroll)
if (isSmallScreen) {
window.addEventListener('scroll', handleScroll)
return () => {
window.removeEventListener('scroll', handleScroll)
}
}
return () => {
scrollArea?.removeEventListener('scroll', handleScroll)
}
}, [lastScrollTop])
scrollAreaRef.current?.addEventListener('scroll', handleScroll)
return () => {
scrollAreaRef.current?.removeEventListener('scroll', handleScroll)
}
}, [lastScrollTop, isSmallScreen, current])
return (
<ScrollArea
ref={scrollAreaRef}
className="h-full w-full"
scrollBarClassName="pt-9 xl:pt-0 max-sm:pt-11"
>
<PrimaryPageTitlebar visible={visible} />
<div className="sm:px-4 pb-4 pt-11 xl:pt-4">{children}</div>
<ScrollToTopButton scrollAreaRef={scrollAreaRef} visible={visible && lastScrollTop > 500} />
</ScrollArea>
)
})
return (
<ScrollArea
className="sm:h-screen sm:overflow-auto"
scrollBarClassName="sm:z-50"
ref={scrollAreaRef}
style={{
paddingBottom: 'env(safe-area-inset-bottom)'
}}
>
{titlebar && (
<PrimaryPageTitlebar visible={!isSmallScreen || visible}>{titlebar}</PrimaryPageTitlebar>
)}
<div className="overflow-x-hidden">{children}</div>
{displayScrollToTopButton && (
<ScrollToTopButton
scrollAreaRef={scrollAreaRef}
visible={visible && lastScrollTop > 500}
/>
)}
{isSmallScreen && <BottomNavigationBar visible={visible} />}
</ScrollArea>
)
}
)
PrimaryPageLayout.displayName = 'PrimaryPageLayout'
export default PrimaryPageLayout
@@ -72,43 +114,16 @@ export type TPrimaryPageLayoutRef = {
scrollToTop: () => void
}
function PrimaryPageTitlebar({ visible = true }: { visible?: boolean }) {
const { isSmallScreen } = useScreenSize()
if (isSmallScreen) {
return (
<Titlebar
className="justify-between px-4 transition-transform duration-500"
visible={visible}
>
<div className="flex gap-1 items-center">
<div className="-translate-y-0.5">
<Logo className="h-8" />
</div>
<ThemeToggle variant="small-screen-titlebar" />
</div>
<div className="flex gap-1 items-center">
<SearchButton variant="small-screen-titlebar" />
<PostButton variant="small-screen-titlebar" />
<RelaySettingsButton variant="small-screen-titlebar" />
<NotificationButton variant="small-screen-titlebar" />
<AccountButton variant="small-screen-titlebar" />
</div>
</Titlebar>
)
}
function PrimaryPageTitlebar({
children,
visible = true
}: {
children?: React.ReactNode
visible?: boolean
}) {
return (
<Titlebar className="justify-between xl:hidden">
<div className="flex gap-2 items-center">
<AccountButton />
<PostButton />
<SearchButton />
</div>
<div className="flex gap-2 items-center">
<RelaySettingsButton />
<NotificationButton />
</div>
<Titlebar className="h-12 p-1" visible={visible}>
{children}
</Titlebar>
)
}