1import {
2 createContext,
3 useContext,
4 useEffect,
5 useLayoutEffect,
6 useState,
7} from 'react'
8import { createStore, useStore } from 'zustand'
9
10import { remToPx } from '@/lib/remToPx'
11
12function createSectionStore(sections) {
13 return createStore((set) => ({
14 sections,
15 visibleSections: [],
16 setVisibleSections: (visibleSections) =>
17 set((state) =>
18 state.visibleSections.join() === visibleSections.join()
19 ? {}
20 : { visibleSections }
21 ),
22 registerHeading: ({ id, ref, offsetRem }) =>
23 set((state) => {
24 return {
25 sections: state.sections.map((section) => {
26 if (section.id === id) {
27 return {
28 ...section,
29 headingRef: ref,
30 offsetRem,
31 }
32 }
33 return section
34 }),
35 }
36 }),
37 }))
38}
39
40function useVisibleSections(sectionStore) {
41 let setVisibleSections = useStore(sectionStore, (s) => s.setVisibleSections)
42 let sections = useStore(sectionStore, (s) => s.sections)
43
44 useEffect(() => {
45 function checkVisibleSections() {
46 let { innerHeight, scrollY } = window
47 let newVisibleSections = []
48
49 for (
50 let sectionIndex = 0;
51 sectionIndex < sections.length;
52 sectionIndex++
53 ) {
54 let { id, headingRef, offsetRem } = sections[sectionIndex]
55 let offset = remToPx(offsetRem)
56 let top = headingRef.current.getBoundingClientRect().top + scrollY
57
58 if (sectionIndex === 0 && top - offset > scrollY) {
59 newVisibleSections.push('_top')
60 }
61
62 let nextSection = sections[sectionIndex + 1]
63 let bottom =
64 (nextSection?.headingRef.current.getBoundingClientRect().top ??
65 Infinity) +
66 scrollY -
67 remToPx(nextSection?.offsetRem ?? 0)
68
69 if (
70 (top > scrollY && top < scrollY + innerHeight) ||
71 (bottom > scrollY && bottom < scrollY + innerHeight) ||
72 (top <= scrollY && bottom >= scrollY + innerHeight)
73 ) {
74 newVisibleSections.push(id)
75 }
76 }
77
78 setVisibleSections(newVisibleSections)
79 }
80
81 let raf = window.requestAnimationFrame(() => checkVisibleSections())
82 window.addEventListener('scroll', checkVisibleSections, { passive: true })
83 window.addEventListener('resize', checkVisibleSections)
84
85 return () => {
86 window.cancelAnimationFrame(raf)
87 window.removeEventListener('scroll', checkVisibleSections)
88 window.removeEventListener('resize', checkVisibleSections)
89 }
90 }, [setVisibleSections, sections])
91}
92
93const SectionStoreContext = createContext()
94
95const useIsomorphicLayoutEffect =
96 typeof window === 'undefined' ? useEffect : useLayoutEffect
97
98export function SectionProvider({ sections, children }) {
99 let [sectionStore] = useState(() => createSectionStore(sections))
100
101 useVisibleSections(sectionStore)
102
103 useIsomorphicLayoutEffect(() => {
104 sectionStore.setState({ sections })
105 }, [sectionStore, sections])
106
107 return (
108 <SectionStoreContext.Provider value={sectionStore}>
109 {children}
110 </SectionStoreContext.Provider>
111 )
112}
113
114export function useSectionStore(selector) {
115 let store = useContext(SectionStoreContext)
116 return useStore(store, selector)
117}