react-fathom 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +199 -0
  3. package/dist/cjs/index.cjs +410 -0
  4. package/dist/cjs/index.cjs.map +1 -0
  5. package/dist/cjs/next/index.cjs +910 -0
  6. package/dist/cjs/next/index.cjs.map +1 -0
  7. package/dist/es/index.js +381 -0
  8. package/dist/es/index.js.map +1 -0
  9. package/dist/es/next/index.js +885 -0
  10. package/dist/es/next/index.js.map +1 -0
  11. package/dist/react-fathom.js +413 -0
  12. package/dist/react-fathom.js.map +1 -0
  13. package/dist/react-fathom.min.js +3 -0
  14. package/dist/react-fathom.min.js.map +1 -0
  15. package/package.json +127 -0
  16. package/src/FathomContext.tsx +5 -0
  17. package/src/FathomProvider.test.tsx +532 -0
  18. package/src/FathomProvider.tsx +122 -0
  19. package/src/components/TrackClick.test.tsx +191 -0
  20. package/src/components/TrackClick.tsx +62 -0
  21. package/src/components/TrackPageview.test.tsx +111 -0
  22. package/src/components/TrackPageview.tsx +36 -0
  23. package/src/components/TrackVisible.test.tsx +311 -0
  24. package/src/components/TrackVisible.tsx +105 -0
  25. package/src/components/index.ts +3 -0
  26. package/src/hooks/index.ts +4 -0
  27. package/src/hooks/useFathom.test.tsx +51 -0
  28. package/src/hooks/useFathom.ts +11 -0
  29. package/src/hooks/useTrackOnClick.test.tsx +197 -0
  30. package/src/hooks/useTrackOnClick.ts +65 -0
  31. package/src/hooks/useTrackOnMount.test.tsx +79 -0
  32. package/src/hooks/useTrackOnMount.ts +24 -0
  33. package/src/hooks/useTrackOnVisible.test.tsx +313 -0
  34. package/src/hooks/useTrackOnVisible.ts +99 -0
  35. package/src/index.ts +4 -0
  36. package/src/next/NextFathomProvider.test.tsx +131 -0
  37. package/src/next/NextFathomProvider.tsx +62 -0
  38. package/src/next/NextFathomProviderApp.test.tsx +308 -0
  39. package/src/next/NextFathomProviderApp.tsx +106 -0
  40. package/src/next/NextFathomProviderPages.test.tsx +330 -0
  41. package/src/next/NextFathomProviderPages.tsx +112 -0
  42. package/src/next/compositions/withAppRouter.test.tsx +113 -0
  43. package/src/next/compositions/withAppRouter.tsx +48 -0
  44. package/src/next/compositions/withPagesRouter.test.tsx +113 -0
  45. package/src/next/compositions/withPagesRouter.tsx +44 -0
  46. package/src/next/index.ts +7 -0
  47. package/src/next/types.ts +19 -0
  48. package/src/types.ts +37 -0
  49. package/types/FathomContext.d.ts +3 -0
  50. package/types/FathomContext.d.ts.map +1 -0
  51. package/types/FathomProvider.d.ts +5 -0
  52. package/types/FathomProvider.d.ts.map +1 -0
  53. package/types/components/TrackClick.d.ts +39 -0
  54. package/types/components/TrackClick.d.ts.map +1 -0
  55. package/types/components/TrackPageview.d.ts +21 -0
  56. package/types/components/TrackPageview.d.ts.map +1 -0
  57. package/types/components/TrackVisible.d.ts +39 -0
  58. package/types/components/TrackVisible.d.ts.map +1 -0
  59. package/types/components/index.d.ts +4 -0
  60. package/types/components/index.d.ts.map +1 -0
  61. package/types/hooks/index.d.ts +5 -0
  62. package/types/hooks/index.d.ts.map +1 -0
  63. package/types/hooks/useFathom.d.ts +6 -0
  64. package/types/hooks/useFathom.d.ts.map +1 -0
  65. package/types/hooks/useTrackOnClick.d.ts +39 -0
  66. package/types/hooks/useTrackOnClick.d.ts.map +1 -0
  67. package/types/hooks/useTrackOnMount.d.ts +14 -0
  68. package/types/hooks/useTrackOnMount.d.ts.map +1 -0
  69. package/types/hooks/useTrackOnVisible.d.ts +43 -0
  70. package/types/hooks/useTrackOnVisible.d.ts.map +1 -0
  71. package/types/index.d.ts +5 -0
  72. package/types/index.d.ts.map +1 -0
  73. package/types/next/AppRouterProvider.d.ts +7 -0
  74. package/types/next/AppRouterProvider.d.ts.map +1 -0
  75. package/types/next/NextFathomProvider.d.ts +34 -0
  76. package/types/next/NextFathomProvider.d.ts.map +1 -0
  77. package/types/next/NextFathomProviderApp.d.ts +6 -0
  78. package/types/next/NextFathomProviderApp.d.ts.map +1 -0
  79. package/types/next/NextFathomProviderPages.d.ts +6 -0
  80. package/types/next/NextFathomProviderPages.d.ts.map +1 -0
  81. package/types/next/PagesRouterProvider.d.ts +7 -0
  82. package/types/next/PagesRouterProvider.d.ts.map +1 -0
  83. package/types/next/compositions/withAppRouter.d.ts +29 -0
  84. package/types/next/compositions/withAppRouter.d.ts.map +1 -0
  85. package/types/next/compositions/withPagesRouter.d.ts +25 -0
  86. package/types/next/compositions/withPagesRouter.d.ts.map +1 -0
  87. package/types/next/index.d.ts +6 -0
  88. package/types/next/index.d.ts.map +1 -0
  89. package/types/next/types.d.ts +16 -0
  90. package/types/next/types.d.ts.map +1 -0
  91. package/types/test-setup.d.ts +2 -0
  92. package/types/test-setup.d.ts.map +1 -0
  93. package/types/types.d.ts +34 -0
  94. package/types/types.d.ts.map +1 -0
  95. package/types/useFathom.d.ts +7 -0
  96. package/types/useFathom.d.ts.map +1 -0
@@ -0,0 +1,313 @@
1
+ import React from 'react'
2
+
3
+ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
4
+
5
+ import { render, renderHook, waitFor } from '@testing-library/react'
6
+
7
+ import { FathomProvider } from '../FathomProvider'
8
+ import { useTrackOnVisible } from './useTrackOnVisible'
9
+
10
+ describe('useTrackOnVisible', () => {
11
+ const mockTrackEvent = vi.fn()
12
+ const mockClient = {
13
+ trackEvent: mockTrackEvent,
14
+ trackPageview: vi.fn(),
15
+ trackGoal: vi.fn(),
16
+ load: vi.fn(),
17
+ setSite: vi.fn(),
18
+ blockTrackingForMe: vi.fn(),
19
+ enableTrackingForMe: vi.fn(),
20
+ isTrackingEnabled: vi.fn(() => true),
21
+ }
22
+
23
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
24
+ <FathomProvider client={mockClient}>{children}</FathomProvider>
25
+ )
26
+
27
+ let mockObserve: ReturnType<typeof vi.fn>
28
+ let mockDisconnect: ReturnType<typeof vi.fn>
29
+ let observerCallback: (entries: IntersectionObserverEntry[]) => void
30
+
31
+ beforeEach(() => {
32
+ vi.clearAllMocks()
33
+ mockObserve = vi.fn()
34
+ mockDisconnect = vi.fn()
35
+
36
+ global.IntersectionObserver = class MockIntersectionObserver {
37
+ observe = mockObserve
38
+ disconnect = mockDisconnect
39
+ unobserve = vi.fn()
40
+
41
+ constructor(callback: (entries: IntersectionObserverEntry[]) => void) {
42
+ observerCallback = callback
43
+ }
44
+ } as any
45
+ })
46
+
47
+ afterEach(() => {
48
+ delete (global as any).IntersectionObserver
49
+ })
50
+
51
+ it('should return a ref object', () => {
52
+ const { result } = renderHook(
53
+ () => useTrackOnVisible({ eventName: 'test-event' }),
54
+ { wrapper },
55
+ )
56
+
57
+ expect(result.current).toHaveProperty('current')
58
+ })
59
+
60
+ it('should create IntersectionObserver when ref is attached', async () => {
61
+ const TestComponent = () => {
62
+ const ref = useTrackOnVisible({ eventName: 'test-event' })
63
+ return <div ref={ref}>Test</div>
64
+ }
65
+
66
+ render(
67
+ <FathomProvider client={mockClient}>
68
+ <TestComponent />
69
+ </FathomProvider>,
70
+ )
71
+
72
+ await waitFor(() => {
73
+ expect(mockObserve).toHaveBeenCalled()
74
+ })
75
+ })
76
+
77
+ it('should track event when element becomes visible', async () => {
78
+ const TestComponent = () => {
79
+ const ref = useTrackOnVisible({ eventName: 'test-event' })
80
+ return <div ref={ref}>Test</div>
81
+ }
82
+
83
+ render(
84
+ <FathomProvider client={mockClient}>
85
+ <TestComponent />
86
+ </FathomProvider>,
87
+ )
88
+
89
+ // Wait for observer to be created
90
+ await waitFor(() => {
91
+ expect(mockObserve).toHaveBeenCalled()
92
+ })
93
+
94
+ // Simulate intersection
95
+ const element = document.querySelector('div')
96
+ const mockEntry = {
97
+ isIntersecting: true,
98
+ target: element!,
99
+ } as IntersectionObserverEntry
100
+
101
+ observerCallback([mockEntry])
102
+
103
+ await waitFor(() => {
104
+ expect(mockTrackEvent).toHaveBeenCalledTimes(1)
105
+ expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {})
106
+ })
107
+ })
108
+
109
+ it('should track event with options', async () => {
110
+ const TestComponent = () => {
111
+ const ref = useTrackOnVisible({
112
+ eventName: 'test-event',
113
+ id: 'test-id',
114
+ value: 100,
115
+ })
116
+ return <div ref={ref}>Test</div>
117
+ }
118
+
119
+ render(
120
+ <FathomProvider client={mockClient}>
121
+ <TestComponent />
122
+ </FathomProvider>,
123
+ )
124
+
125
+ // Wait for observer to be created
126
+ await waitFor(() => {
127
+ expect(mockObserve).toHaveBeenCalled()
128
+ })
129
+
130
+ const element = document.querySelector('div')
131
+ const mockEntry = {
132
+ isIntersecting: true,
133
+ target: element!,
134
+ } as IntersectionObserverEntry
135
+
136
+ observerCallback([mockEntry])
137
+
138
+ await waitFor(() => {
139
+ expect(mockTrackEvent).toHaveBeenCalledWith('test-event', {
140
+ id: 'test-id',
141
+ value: 100,
142
+ })
143
+ })
144
+ })
145
+
146
+ it('should track only once when trackOnce is true', async () => {
147
+ const TestComponent = () => {
148
+ const ref = useTrackOnVisible({
149
+ eventName: 'test-event',
150
+ trackOnce: true,
151
+ })
152
+ return <div ref={ref}>Test</div>
153
+ }
154
+
155
+ render(
156
+ <FathomProvider client={mockClient}>
157
+ <TestComponent />
158
+ </FathomProvider>,
159
+ )
160
+
161
+ // Wait for observer to be created
162
+ await waitFor(() => {
163
+ expect(mockObserve).toHaveBeenCalled()
164
+ })
165
+
166
+ const element = document.querySelector('div')
167
+ const mockEntry = {
168
+ isIntersecting: true,
169
+ target: element!,
170
+ } as IntersectionObserverEntry
171
+
172
+ observerCallback([mockEntry])
173
+ observerCallback([mockEntry])
174
+ observerCallback([mockEntry])
175
+
176
+ await waitFor(() => {
177
+ expect(mockTrackEvent).toHaveBeenCalledTimes(1)
178
+ })
179
+ })
180
+
181
+ it('should track multiple times when trackOnce is false', async () => {
182
+ const TestComponent = () => {
183
+ const ref = useTrackOnVisible({
184
+ eventName: 'test-event',
185
+ trackOnce: false,
186
+ })
187
+ return <div ref={ref}>Test</div>
188
+ }
189
+
190
+ render(
191
+ <FathomProvider client={mockClient}>
192
+ <TestComponent />
193
+ </FathomProvider>,
194
+ )
195
+
196
+ // Wait for observer to be created
197
+ await waitFor(() => {
198
+ expect(mockObserve).toHaveBeenCalled()
199
+ })
200
+
201
+ const element = document.querySelector('div')
202
+ const mockEntry = {
203
+ isIntersecting: true,
204
+ target: element!,
205
+ } as IntersectionObserverEntry
206
+
207
+ observerCallback([mockEntry])
208
+ observerCallback([mockEntry])
209
+ observerCallback([mockEntry])
210
+
211
+ await waitFor(() => {
212
+ expect(mockTrackEvent).toHaveBeenCalledTimes(3)
213
+ })
214
+ })
215
+
216
+ it('should call callback when element becomes visible', async () => {
217
+ const callback = vi.fn()
218
+
219
+ const TestComponent = () => {
220
+ const ref = useTrackOnVisible({
221
+ eventName: 'test-event',
222
+ callback,
223
+ })
224
+ return <div ref={ref}>Test</div>
225
+ }
226
+
227
+ render(
228
+ <FathomProvider client={mockClient}>
229
+ <TestComponent />
230
+ </FathomProvider>,
231
+ )
232
+
233
+ // Wait for observer to be created
234
+ await waitFor(() => {
235
+ expect(mockObserve).toHaveBeenCalled()
236
+ })
237
+
238
+ const element = document.querySelector('div')
239
+ const mockEntry = {
240
+ isIntersecting: true,
241
+ target: element!,
242
+ } as IntersectionObserverEntry
243
+
244
+ observerCallback([mockEntry])
245
+
246
+ await waitFor(() => {
247
+ expect(callback).toHaveBeenCalledTimes(1)
248
+ expect(callback).toHaveBeenCalledWith(mockEntry)
249
+ })
250
+ })
251
+
252
+ it('should use custom observer options', async () => {
253
+ const observerOptions = { threshold: 0.5 }
254
+ let capturedOptions: IntersectionObserverInit | undefined
255
+
256
+ const OriginalObserver = global.IntersectionObserver
257
+ global.IntersectionObserver = class MockIntersectionObserver {
258
+ observe = mockObserve
259
+ disconnect = mockDisconnect
260
+ unobserve = vi.fn()
261
+
262
+ constructor(
263
+ callback: (entries: IntersectionObserverEntry[]) => void,
264
+ options?: IntersectionObserverInit,
265
+ ) {
266
+ capturedOptions = options
267
+ observerCallback = callback
268
+ }
269
+ } as any
270
+
271
+ const TestComponent = () => {
272
+ const ref = useTrackOnVisible({
273
+ eventName: 'test-event',
274
+ observerOptions,
275
+ })
276
+ return <div ref={ref}>Test</div>
277
+ }
278
+
279
+ render(
280
+ <FathomProvider client={mockClient}>
281
+ <TestComponent />
282
+ </FathomProvider>,
283
+ )
284
+
285
+ await waitFor(() => {
286
+ expect(capturedOptions?.threshold).toBe(0.5)
287
+ })
288
+
289
+ global.IntersectionObserver = OriginalObserver
290
+ })
291
+
292
+ it('should disconnect observer on unmount', async () => {
293
+ const TestComponent = () => {
294
+ const ref = useTrackOnVisible({ eventName: 'test-event' })
295
+ return <div ref={ref}>Test</div>
296
+ }
297
+
298
+ const { unmount } = render(
299
+ <FathomProvider client={mockClient}>
300
+ <TestComponent />
301
+ </FathomProvider>,
302
+ )
303
+
304
+ // Wait for observer to be created
305
+ await waitFor(() => {
306
+ expect(mockObserve).toHaveBeenCalled()
307
+ })
308
+
309
+ unmount()
310
+
311
+ expect(mockDisconnect).toHaveBeenCalledTimes(1)
312
+ })
313
+ })
@@ -0,0 +1,99 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { RefObject } from 'react'
3
+
4
+ import type { EventOptions } from 'fathom-client'
5
+
6
+ import { useFathom } from './useFathom'
7
+
8
+ export interface UseTrackOnVisibleOptions extends EventOptions {
9
+ /**
10
+ * Event name to track
11
+ */
12
+ eventName: string
13
+ /**
14
+ * Intersection observer options
15
+ */
16
+ observerOptions?: IntersectionObserverInit
17
+ /**
18
+ * Whether to track only once or every time it becomes visible
19
+ * @default true
20
+ */
21
+ trackOnce?: boolean
22
+ /**
23
+ * Optional callback function to run after tracking
24
+ * Receives the intersection observer entry as a parameter
25
+ */
26
+ callback?: (entry: IntersectionObserverEntry) => void
27
+ }
28
+
29
+ /**
30
+ * Hook to track an event when an element becomes visible (using Intersection Observer)
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * function Section() {
35
+ * const ref = useTrackOnVisible({
36
+ * eventName: 'section-viewed',
37
+ * section: 'hero',
38
+ * callback: (entry) => {
39
+ * console.log('Section is visible!', entry.isIntersecting)
40
+ * // Your custom logic here
41
+ * },
42
+ * })
43
+ *
44
+ * return <section ref={ref}>Content</section>
45
+ * }
46
+ * ```
47
+ */
48
+ export const useTrackOnVisible = (
49
+ options: UseTrackOnVisibleOptions,
50
+ ): RefObject<HTMLElement | null> => {
51
+ const { trackEvent } = useFathom()
52
+ const {
53
+ eventName,
54
+ observerOptions,
55
+ trackOnce = true,
56
+ callback,
57
+ ...eventOptions
58
+ } = options
59
+ const ref = useRef<HTMLElement | null>(null)
60
+ const hasTracked = useRef(false)
61
+
62
+ useEffect(() => {
63
+ const element = ref.current
64
+ if (!element) return
65
+
66
+ const observer = new IntersectionObserver(
67
+ (entries) => {
68
+ entries.forEach((entry) => {
69
+ if (entry.isIntersecting) {
70
+ if (!trackOnce || !hasTracked.current) {
71
+ trackEvent?.(eventName, eventOptions)
72
+ callback?.(entry)
73
+ hasTracked.current = true
74
+ }
75
+ }
76
+ })
77
+ },
78
+ {
79
+ threshold: 0.1,
80
+ ...observerOptions,
81
+ },
82
+ )
83
+
84
+ observer.observe(element)
85
+
86
+ return () => {
87
+ observer.disconnect()
88
+ }
89
+ }, [
90
+ eventName,
91
+ eventOptions,
92
+ observerOptions,
93
+ trackOnce,
94
+ trackEvent,
95
+ callback,
96
+ ])
97
+
98
+ return ref
99
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './FathomContext'
2
+ export * from './FathomProvider'
3
+ export * from './hooks'
4
+ export * from './components'
@@ -0,0 +1,131 @@
1
+ import React from 'react'
2
+
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import { render, screen, waitFor } from '@testing-library/react'
6
+
7
+ import { NextFathomProvider } from './NextFathomProvider'
8
+
9
+ // Mock the lazy-loaded providers
10
+ vi.mock('./NextFathomProviderApp', () => ({
11
+ default: ({
12
+ children,
13
+ siteId,
14
+ }: {
15
+ children: React.ReactNode
16
+ siteId?: string
17
+ }) => (
18
+ <div data-testid="app-provider" data-site-id={siteId}>
19
+ {children}
20
+ </div>
21
+ ),
22
+ }))
23
+
24
+ vi.mock('./NextFathomProviderPages', () => ({
25
+ default: ({
26
+ children,
27
+ siteId,
28
+ }: {
29
+ children: React.ReactNode
30
+ siteId?: string
31
+ }) => (
32
+ <div data-testid="pages-provider" data-site-id={siteId}>
33
+ {children}
34
+ </div>
35
+ ),
36
+ }))
37
+
38
+ describe('NextFathomProvider', () => {
39
+ beforeEach(() => {
40
+ vi.clearAllMocks()
41
+ })
42
+
43
+ it('should render App Router provider by default', async () => {
44
+ render(
45
+ <NextFathomProvider siteId="TEST_SITE_ID">
46
+ <div>Test Content</div>
47
+ </NextFathomProvider>,
48
+ )
49
+
50
+ await waitFor(() => {
51
+ expect(screen.getByTestId('app-provider')).toBeInTheDocument()
52
+ expect(screen.getByTestId('app-provider')).toHaveAttribute(
53
+ 'data-site-id',
54
+ 'TEST_SITE_ID',
55
+ )
56
+ })
57
+
58
+ expect(screen.getByText('Test Content')).toBeInTheDocument()
59
+ })
60
+
61
+ it('should render App Router provider when router="app"', async () => {
62
+ render(
63
+ <NextFathomProvider siteId="TEST_SITE_ID" router="app">
64
+ <div>Test Content</div>
65
+ </NextFathomProvider>,
66
+ )
67
+
68
+ await waitFor(() => {
69
+ expect(screen.getByTestId('app-provider')).toBeInTheDocument()
70
+ })
71
+
72
+ expect(screen.queryByTestId('pages-provider')).not.toBeInTheDocument()
73
+ })
74
+
75
+ it('should render Pages Router provider when router="pages"', async () => {
76
+ render(
77
+ <NextFathomProvider siteId="TEST_SITE_ID" router="pages">
78
+ <div>Test Content</div>
79
+ </NextFathomProvider>,
80
+ )
81
+
82
+ await waitFor(() => {
83
+ expect(screen.getByTestId('pages-provider')).toBeInTheDocument()
84
+ expect(screen.getByTestId('pages-provider')).toHaveAttribute(
85
+ 'data-site-id',
86
+ 'TEST_SITE_ID',
87
+ )
88
+ })
89
+
90
+ expect(screen.queryByTestId('app-provider')).not.toBeInTheDocument()
91
+ expect(screen.getByText('Test Content')).toBeInTheDocument()
92
+ })
93
+
94
+ it('should render custom fallback while loading', async () => {
95
+ render(
96
+ <NextFathomProvider
97
+ siteId="TEST_SITE_ID"
98
+ fallback={<div>Loading...</div>}
99
+ >
100
+ <div>Test Content</div>
101
+ </NextFathomProvider>,
102
+ )
103
+
104
+ // Suspense fallback may be shown briefly, but provider should load quickly
105
+ await waitFor(() => {
106
+ expect(screen.getByTestId('app-provider')).toBeInTheDocument()
107
+ })
108
+ })
109
+
110
+ it('should pass all props to the underlying provider', async () => {
111
+ const clientOptions = { honorDNT: true }
112
+ render(
113
+ <NextFathomProvider
114
+ siteId="TEST_SITE_ID"
115
+ router="app"
116
+ clientOptions={clientOptions}
117
+ disableAutoTrack
118
+ >
119
+ <div>Test Content</div>
120
+ </NextFathomProvider>,
121
+ )
122
+
123
+ await waitFor(() => {
124
+ expect(screen.getByTestId('app-provider')).toBeInTheDocument()
125
+ })
126
+ })
127
+
128
+ it('should have displayName', () => {
129
+ expect(NextFathomProvider.displayName).toBe('NextFathomProvider')
130
+ })
131
+ })
@@ -0,0 +1,62 @@
1
+ import React, { Suspense, lazy, useMemo } from 'react'
2
+
3
+ import type { NextFathomProviderProps } from './types'
4
+
5
+ export interface NextFathomProviderComponentProps extends NextFathomProviderProps {
6
+ /**
7
+ * Router type to use
8
+ * @default 'app'
9
+ */
10
+ router?: 'pages' | 'app'
11
+ /**
12
+ * Fallback component to show while loading the router provider
13
+ * @default null
14
+ */
15
+ fallback?: React.ReactNode
16
+ }
17
+
18
+ // Dynamically import providers to enable code-splitting
19
+ const NextFathomProviderAppLazy = lazy(
20
+ async () => await import('./NextFathomProviderApp'),
21
+ )
22
+ const NextFathomProviderPagesLazy = lazy(
23
+ async () => await import('./NextFathomProviderPages'),
24
+ )
25
+
26
+ /**
27
+ * Unified provider component that conditionally renders the appropriate
28
+ * Next.js router provider based on the `router` prop.
29
+ * Providers are dynamically loaded to avoid bundling both router types.
30
+ *
31
+ * @example
32
+ * ```tsx
33
+ * // App Router (default)
34
+ * <NextFathomProvider siteId="YOUR_SITE_ID" router="app">
35
+ * <App>{children}</App>
36
+ * </NextFathomProvider>
37
+ *
38
+ * // Pages Router
39
+ * <NextFathomProvider siteId="YOUR_SITE_ID" router="pages">
40
+ * <App>{children}</App>
41
+ * </NextFathomProvider>
42
+ * ```
43
+ */
44
+ export const NextFathomProvider: React.FC<NextFathomProviderComponentProps> = ({
45
+ router = 'app',
46
+ fallback = null,
47
+ ...props
48
+ }) => {
49
+ const Provider = useMemo(() => {
50
+ return router === 'pages'
51
+ ? NextFathomProviderPagesLazy
52
+ : NextFathomProviderAppLazy
53
+ }, [router])
54
+
55
+ return (
56
+ <Suspense fallback={fallback}>
57
+ <Provider {...props} />
58
+ </Suspense>
59
+ )
60
+ }
61
+
62
+ NextFathomProvider.displayName = 'NextFathomProvider'