Skip to content

Instantly share code, notes, and snippets.

@kentcdodds
Last active June 9, 2021 04:24
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kentcdodds/c0b14f53f33c9ffb833d1bd8ba61b3eb to your computer and use it in GitHub Desktop.
Save kentcdodds/c0b14f53f33c9ffb833d1bd8ba61b3eb to your computer and use it in GitHub Desktop.
How I determine whether you've read a blog post.
function useOnRead({
parentElRef,
onRead,
enabled = true,
}: {
parentElRef: React.RefObject<HTMLElement>
onRead: () => void
enabled: boolean
}) {
React.useEffect(() => {
const parentEl = parentElRef.current
if (!enabled || !parentEl || !parentEl.textContent) return
// calculateReadingTime comes from https://npm.im/reading-time
const readingTime = calculateReadingTime(parentEl.textContent)
const visibilityEl = document.createElement('div')
let scrolledTheMain = false
const observer = new IntersectionObserver(entries => {
const isVisible = entries.some(entry => {
return entry.target === visibilityEl && entry.isIntersecting
})
if (isVisible) {
scrolledTheMain = true
maybeMarkAsRead()
observer.disconnect()
visibilityEl.remove()
}
})
let startTime = new Date().getTime()
let timeoutTime = readingTime.time * 0.6
let timerId: ReturnType<typeof setTimeout>
let timerFinished = false
function startTimer() {
timerId = setTimeout(() => {
timerFinished = true
document.removeEventListener('visibilitychange', handleVisibilityChange)
maybeMarkAsRead()
}, timeoutTime)
}
function handleVisibilityChange() {
if (document.hidden) {
clearTimeout(timerId)
const timeElapsedSoFar = new Date().getTime() - startTime
timeoutTime = timeoutTime - timeElapsedSoFar
} else {
startTime = new Date().getTime()
startTimer()
}
}
function maybeMarkAsRead() {
if (timerFinished && scrolledTheMain) {
cleanup()
onRead()
}
}
// dirty-up
parentEl.append(visibilityEl)
observer.observe(visibilityEl)
startTimer()
document.addEventListener('visibilitychange', handleVisibilityChange)
function cleanup() {
document.removeEventListener('visibilitychange', handleVisibilityChange)
clearTimeout(timerId)
observer.disconnect()
visibilityEl.remove()
}
return cleanup
}, [enabled, onRead, parentElRef])
}
function MdxScreen() {
const data = useRouteData<LoaderData>()
if (!data.page) {
throw new Error(
'This should be impossible because we only render the MdxScreen if there is a data.page object.',
)
}
const {code, frontmatter} = data.page
const params = useParams()
const {slug} = params
const Component = React.useMemo(() => getMdxComponent(code), [code])
const mainRef = React.useRef<HTMLDivElement>(null)
useOnRead({
parentElRef: mainRef,
onRead: React.useCallback(() => {
const searchParams = new URLSearchParams([
['_data', 'routes/_action/mark-read'],
])
void fetch(`/_action/mark-read?${searchParams}`, {
method: 'POST',
body: JSON.stringify({articleSlug: slug}),
})
}, [slug]),
enabled: Boolean(data.user),
})
return (
<>
<header>
<h1>{frontmatter.meta.title}</h1>
<p>{frontmatter.meta.description}</p>
</header>
<main ref={mainRef}>
<Component />
</main>
</>
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment