Last active
June 9, 2021 04:24
-
-
Save kentcdodds/c0b14f53f33c9ffb833d1bd8ba61b3eb to your computer and use it in GitHub Desktop.
How I determine whether you've read a blog post.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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