feat: pull-to-refresh for PWA without native refresh

This commit is contained in:
Soxasora 2024-11-17 16:48:42 +01:00
parent 83e72e21cc
commit eea121e30c
3 changed files with 120 additions and 10 deletions

View File

@ -6,6 +6,7 @@ import Footer from './footer'
import Seo, { SeoSearch } from './seo'
import Search from './search'
import styles from './layout.module.css'
import PullToRefresh from './pull-to-refresh'
export default function Layout ({
sub, contain = true, footer = true, footerLinks = true,
@ -14,16 +15,18 @@ export default function Layout ({
return (
<>
{seo && <Seo sub={sub} item={item} user={user} />}
<Navigation sub={sub} />
{contain
? (
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
{children}
</Container>
)
: children}
{footer && <Footer links={footerLinks} />}
<NavFooter sub={sub} />
<PullToRefresh>
<Navigation sub={sub} />
{contain
? (
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
{children}
</Container>
)
: children}
{footer && <Footer links={footerLinks} />}
<NavFooter sub={sub} />
</PullToRefresh>
</>
)
}

View File

@ -0,0 +1,86 @@
import { useRouter } from 'next/router'
import { useState, useRef, useEffect, useCallback } from 'react'
import styles from './pull-to-refresh.module.css'
export default function PullToRefresh ({ children }) {
const router = useRouter()
const [isRefreshing, setIsRefreshing] = useState(false)
const [pullDistance, setPullDistance] = useState(0)
const [isPWA, setIsPWA] = useState(false)
const touchStartY = useRef(0)
const touchEndY = useRef(0)
useEffect(() => {
const checkPWA = () => {
// android/general || ios
return window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true
}
setIsPWA(checkPWA())
}, [])
const handleTouchStart = useCallback((e) => {
if (!isPWA || window.scrollY > 0) return // don't handle if the user is not scrolling from the top of the page
touchStartY.current = e.touches[0].clientY
}, [isPWA])
const handleTouchMove = useCallback((e) => {
if (touchStartY.current === 0) return
touchEndY.current = e.touches[0].clientY
setPullDistance(touchEndY.current - touchStartY.current)
}, [])
const handleTouchEnd = useCallback(() => {
if (touchStartY.current === 0 || touchEndY.current === 0) return
if (touchEndY.current - touchStartY.current > 300) { // current threshold is 300, subject to change
setIsRefreshing(true)
router.push(router.asPath)
setTimeout(() => {
setIsRefreshing(false)
}, 500) // simulate loading time
}
setPullDistance(0) // using this to reset the message behavior
touchStartY.current = 0 // avoid random refreshes by resetting touch
touchEndY.current = 0
}, [router])
useEffect(() => {
if (!isPWA) return
document.addEventListener('touchstart', handleTouchStart)
document.addEventListener('touchmove', handleTouchMove)
document.addEventListener('touchend', handleTouchEnd)
return () => {
document.removeEventListener('touchstart', handleTouchStart)
document.removeEventListener('touchmove', handleTouchMove)
document.removeEventListener('touchend', handleTouchEnd)
}
}, [isPWA, handleTouchStart, handleTouchMove, handleTouchEnd])
const getPullMessage = () => {
if (isRefreshing) return 'refreshing...'
if (pullDistance > 300) return 'release to refresh'
if (pullDistance > 0) return 'pull down to refresh'
return ''
}
return (
<div>
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{pullDistance > 0 || isRefreshing
? (
<>
<p className={`${styles.pullMessage} ${pullDistance > 50 || isRefreshing ? styles.fadeIn : ''}`}>
{getPullMessage()}
</p>
{isRefreshing && <div className={styles.spacer} />}
</>
)
: null}
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,21 @@
.pullMessage {
position: fixed;
left: 50%;
top: -50px;
transform: translateX(-50%);
font-size: 18px;
color: #a5a5a5;
opacity: 0;
transition: opacity 0.3s ease-in-out;
text-align: center;
}
.fadeIn {
opacity: 1;
top: 0;
}
.spacer {
position: relative;
margin-bottom: 32px;
}