diff --git a/components/layout.js b/components/layout.js
index f41e6397..15619e02 100644
--- a/components/layout.js
+++ b/components/layout.js
@@ -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 && }
-
- {contain
- ? (
-
- {children}
-
- )
- : children}
- {footer && }
-
+
+
+ {contain
+ ? (
+
+ {children}
+
+ )
+ : children}
+ {footer && }
+
+
>
)
}
diff --git a/components/pull-to-refresh.js b/components/pull-to-refresh.js
new file mode 100644
index 00000000..8b67012e
--- /dev/null
+++ b/components/pull-to-refresh.js
@@ -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 (
+
+
+ {pullDistance > 0 || isRefreshing
+ ? (
+ <>
+
50 || isRefreshing ? styles.fadeIn : ''}`}>
+ {getPullMessage()}
+
+ {isRefreshing &&
}
+ >
+ )
+ : null}
+ {children}
+
+
+ )
+}
diff --git a/components/pull-to-refresh.module.css b/components/pull-to-refresh.module.css
new file mode 100644
index 00000000..c872581e
--- /dev/null
+++ b/components/pull-to-refresh.module.css
@@ -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;
+}
\ No newline at end of file