feat: pull-to-refresh for PWA without native refresh
This commit is contained in:
parent
83e72e21cc
commit
eea121e30c
|
@ -6,6 +6,7 @@ import Footer from './footer'
|
||||||
import Seo, { SeoSearch } from './seo'
|
import Seo, { SeoSearch } from './seo'
|
||||||
import Search from './search'
|
import Search from './search'
|
||||||
import styles from './layout.module.css'
|
import styles from './layout.module.css'
|
||||||
|
import PullToRefresh from './pull-to-refresh'
|
||||||
|
|
||||||
export default function Layout ({
|
export default function Layout ({
|
||||||
sub, contain = true, footer = true, footerLinks = true,
|
sub, contain = true, footer = true, footerLinks = true,
|
||||||
|
@ -14,16 +15,18 @@ export default function Layout ({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{seo && <Seo sub={sub} item={item} user={user} />}
|
{seo && <Seo sub={sub} item={item} user={user} />}
|
||||||
<Navigation sub={sub} />
|
<PullToRefresh>
|
||||||
{contain
|
<Navigation sub={sub} />
|
||||||
? (
|
{contain
|
||||||
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
|
? (
|
||||||
{children}
|
<Container as='main' className={`px-sm-0 ${styles.contain}`}>
|
||||||
</Container>
|
{children}
|
||||||
)
|
</Container>
|
||||||
: children}
|
)
|
||||||
{footer && <Footer links={footerLinks} />}
|
: children}
|
||||||
<NavFooter sub={sub} />
|
{footer && <Footer links={footerLinks} />}
|
||||||
|
<NavFooter sub={sub} />
|
||||||
|
</PullToRefresh>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue