@od-oneapp/analytics 2026.1.1301

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 (184) hide show
  1. package/README.md +509 -0
  2. package/dist/ai-YMnynb-t.mjs +3347 -0
  3. package/dist/ai-YMnynb-t.mjs.map +1 -0
  4. package/dist/chunk-DQk6qfdC.mjs +18 -0
  5. package/dist/client-CTzJVFU5.mjs +9 -0
  6. package/dist/client-CTzJVFU5.mjs.map +1 -0
  7. package/dist/client-CcFTauAh.mjs +54 -0
  8. package/dist/client-CcFTauAh.mjs.map +1 -0
  9. package/dist/client-CeOLjbac.mjs +281 -0
  10. package/dist/client-CeOLjbac.mjs.map +1 -0
  11. package/dist/client-D339NFJS.mjs +267 -0
  12. package/dist/client-D339NFJS.mjs.map +1 -0
  13. package/dist/client-next.d.mts +62 -0
  14. package/dist/client-next.d.mts.map +1 -0
  15. package/dist/client-next.mjs +525 -0
  16. package/dist/client-next.mjs.map +1 -0
  17. package/dist/client.d.mts +30 -0
  18. package/dist/client.d.mts.map +1 -0
  19. package/dist/client.mjs +186 -0
  20. package/dist/client.mjs.map +1 -0
  21. package/dist/config-DPS6bSYo.d.mts +34 -0
  22. package/dist/config-DPS6bSYo.d.mts.map +1 -0
  23. package/dist/config-P6P5adJg.mjs +287 -0
  24. package/dist/config-P6P5adJg.mjs.map +1 -0
  25. package/dist/console-8bND3mMU.mjs +128 -0
  26. package/dist/console-8bND3mMU.mjs.map +1 -0
  27. package/dist/ecommerce-Cgu4wlux.mjs +993 -0
  28. package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
  29. package/dist/emitters-6-nKo8i-.mjs +208 -0
  30. package/dist/emitters-6-nKo8i-.mjs.map +1 -0
  31. package/dist/emitters-DldkVSPp.d.mts +12 -0
  32. package/dist/emitters-DldkVSPp.d.mts.map +1 -0
  33. package/dist/index-BfNWgfa5.d.mts +1494 -0
  34. package/dist/index-BfNWgfa5.d.mts.map +1 -0
  35. package/dist/index-BkIWe--N.d.mts +953 -0
  36. package/dist/index-BkIWe--N.d.mts.map +1 -0
  37. package/dist/index-jPzXRn52.d.mts +184 -0
  38. package/dist/index-jPzXRn52.d.mts.map +1 -0
  39. package/dist/manager-DvRRjza6.d.mts +76 -0
  40. package/dist/manager-DvRRjza6.d.mts.map +1 -0
  41. package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
  42. package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
  43. package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
  44. package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
  45. package/dist/providers-http-client.d.mts +37 -0
  46. package/dist/providers-http-client.d.mts.map +1 -0
  47. package/dist/providers-http-client.mjs +320 -0
  48. package/dist/providers-http-client.mjs.map +1 -0
  49. package/dist/providers-http-server.d.mts +31 -0
  50. package/dist/providers-http-server.d.mts.map +1 -0
  51. package/dist/providers-http-server.mjs +297 -0
  52. package/dist/providers-http-server.mjs.map +1 -0
  53. package/dist/providers-http.d.mts +46 -0
  54. package/dist/providers-http.d.mts.map +1 -0
  55. package/dist/providers-http.mjs +4 -0
  56. package/dist/server-edge.d.mts +9 -0
  57. package/dist/server-edge.d.mts.map +1 -0
  58. package/dist/server-edge.mjs +373 -0
  59. package/dist/server-edge.mjs.map +1 -0
  60. package/dist/server-next.d.mts +67 -0
  61. package/dist/server-next.d.mts.map +1 -0
  62. package/dist/server-next.mjs +193 -0
  63. package/dist/server-next.mjs.map +1 -0
  64. package/dist/server.d.mts +10 -0
  65. package/dist/server.mjs +7 -0
  66. package/dist/service-cYtBBL8x.mjs +945 -0
  67. package/dist/service-cYtBBL8x.mjs.map +1 -0
  68. package/dist/shared.d.mts +16 -0
  69. package/dist/shared.d.mts.map +1 -0
  70. package/dist/shared.mjs +93 -0
  71. package/dist/shared.mjs.map +1 -0
  72. package/dist/types-BxBnNQ0V.d.mts +354 -0
  73. package/dist/types-BxBnNQ0V.d.mts.map +1 -0
  74. package/dist/types-CBvxUEaF.d.mts +216 -0
  75. package/dist/types-CBvxUEaF.d.mts.map +1 -0
  76. package/dist/types.d.mts +4 -0
  77. package/dist/types.mjs +0 -0
  78. package/dist/vercel-types-lwakUfoI.d.mts +102 -0
  79. package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
  80. package/package.json +129 -0
  81. package/src/client/index.ts +164 -0
  82. package/src/client/manager.ts +71 -0
  83. package/src/client/next/components.tsx +270 -0
  84. package/src/client/next/hooks.ts +217 -0
  85. package/src/client/next/manager.ts +141 -0
  86. package/src/client/next.ts +144 -0
  87. package/src/client-next.ts +101 -0
  88. package/src/client.ts +89 -0
  89. package/src/examples/ai-sdk-patterns.ts +583 -0
  90. package/src/examples/emitter-patterns.ts +476 -0
  91. package/src/examples/nextjs-emitter-patterns.tsx +403 -0
  92. package/src/next/app-router.tsx +564 -0
  93. package/src/next/client.ts +419 -0
  94. package/src/next/index.ts +84 -0
  95. package/src/next/middleware.ts +429 -0
  96. package/src/next/rsc.tsx +300 -0
  97. package/src/next/server.ts +253 -0
  98. package/src/next/types.d.ts +220 -0
  99. package/src/providers/base-provider.ts +419 -0
  100. package/src/providers/console/client.ts +10 -0
  101. package/src/providers/console/index.ts +152 -0
  102. package/src/providers/console/server.ts +6 -0
  103. package/src/providers/console/types.ts +15 -0
  104. package/src/providers/http/client.ts +464 -0
  105. package/src/providers/http/index.ts +30 -0
  106. package/src/providers/http/server.ts +396 -0
  107. package/src/providers/http/types.ts +135 -0
  108. package/src/providers/posthog/client.ts +518 -0
  109. package/src/providers/posthog/index.ts +11 -0
  110. package/src/providers/posthog/server.ts +329 -0
  111. package/src/providers/posthog/types.ts +104 -0
  112. package/src/providers/segment/client.ts +113 -0
  113. package/src/providers/segment/index.ts +11 -0
  114. package/src/providers/segment/server.ts +115 -0
  115. package/src/providers/segment/types.ts +51 -0
  116. package/src/providers/vercel/client.ts +102 -0
  117. package/src/providers/vercel/index.ts +11 -0
  118. package/src/providers/vercel/server.ts +89 -0
  119. package/src/providers/vercel/types.ts +27 -0
  120. package/src/server/index.ts +103 -0
  121. package/src/server/manager.ts +62 -0
  122. package/src/server/next.ts +210 -0
  123. package/src/server-edge.ts +442 -0
  124. package/src/server-next.ts +39 -0
  125. package/src/server.ts +106 -0
  126. package/src/shared/emitters/ai/README.md +981 -0
  127. package/src/shared/emitters/ai/events/agent.ts +130 -0
  128. package/src/shared/emitters/ai/events/artifacts.ts +167 -0
  129. package/src/shared/emitters/ai/events/chat.ts +126 -0
  130. package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
  131. package/src/shared/emitters/ai/events/completion.ts +103 -0
  132. package/src/shared/emitters/ai/events/content-generation.ts +347 -0
  133. package/src/shared/emitters/ai/events/conversation.ts +332 -0
  134. package/src/shared/emitters/ai/events/product-features.ts +1402 -0
  135. package/src/shared/emitters/ai/events/streaming.ts +114 -0
  136. package/src/shared/emitters/ai/events/tool.ts +93 -0
  137. package/src/shared/emitters/ai/index.ts +69 -0
  138. package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
  139. package/src/shared/emitters/ai/track-ai.ts +50 -0
  140. package/src/shared/emitters/ai/types.ts +1041 -0
  141. package/src/shared/emitters/ai/utils.ts +468 -0
  142. package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
  143. package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
  144. package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
  145. package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
  146. package/src/shared/emitters/ecommerce/events/order.ts +199 -0
  147. package/src/shared/emitters/ecommerce/events/product.ts +205 -0
  148. package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
  149. package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
  150. package/src/shared/emitters/ecommerce/index.ts +46 -0
  151. package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
  152. package/src/shared/emitters/ecommerce/types.ts +314 -0
  153. package/src/shared/emitters/ecommerce/utils.ts +216 -0
  154. package/src/shared/emitters/emitter-types.ts +974 -0
  155. package/src/shared/emitters/emitters.ts +292 -0
  156. package/src/shared/emitters/helpers.ts +419 -0
  157. package/src/shared/emitters/index.ts +66 -0
  158. package/src/shared/index.ts +142 -0
  159. package/src/shared/ingestion/index.ts +66 -0
  160. package/src/shared/ingestion/schemas.ts +386 -0
  161. package/src/shared/ingestion/service.ts +628 -0
  162. package/src/shared/node22-features.ts +848 -0
  163. package/src/shared/providers/console-provider.ts +160 -0
  164. package/src/shared/types/base-types.ts +54 -0
  165. package/src/shared/types/console-types.ts +19 -0
  166. package/src/shared/types/posthog-types.ts +131 -0
  167. package/src/shared/types/segment-types.ts +15 -0
  168. package/src/shared/types/types.ts +397 -0
  169. package/src/shared/types/vercel-types.ts +19 -0
  170. package/src/shared/utils/config-client.ts +19 -0
  171. package/src/shared/utils/config.ts +250 -0
  172. package/src/shared/utils/emitter-adapter.ts +212 -0
  173. package/src/shared/utils/manager.test.ts +36 -0
  174. package/src/shared/utils/manager.ts +1322 -0
  175. package/src/shared/utils/posthog-bootstrap.ts +136 -0
  176. package/src/shared/utils/posthog-client-utils.ts +48 -0
  177. package/src/shared/utils/posthog-next-utils.ts +282 -0
  178. package/src/shared/utils/posthog-server-utils.ts +210 -0
  179. package/src/shared/utils/rate-limit.ts +289 -0
  180. package/src/shared/utils/security.ts +545 -0
  181. package/src/shared/utils/validation-client.ts +161 -0
  182. package/src/shared/utils/validation.ts +399 -0
  183. package/src/shared.ts +155 -0
  184. package/src/types/index.ts +62 -0
@@ -0,0 +1,403 @@
1
+ /**
2
+ * @fileoverview Next.js Emitter Patterns - Examples for App Router
3
+ * Next.js Emitter Patterns - Examples for App Router
4
+ *
5
+ * This file demonstrates emitter usage patterns specifically
6
+ * for Next.js 15 applications using the App Router.
7
+ *
8
+ * NOTE: This is an example file. In production, use dependency injection
9
+ * to avoid Next.js imports in package source.
10
+ */
11
+
12
+ // Example shows pattern - in real usage, pass cookies as parameter
13
+ // import { cookies } from 'next/headers'; // Remove direct import
14
+ import React from 'react';
15
+
16
+ import { TrackedButton, TrackedLink, useAnalytics, useTrackEvent } from '../next/app-router';
17
+ import { identifyServerUser, trackEventAction, trackServerEvent } from '../next/rsc';
18
+ import { createUserSession, EventBatch, page, track, withMetadata } from '../shared/emitters';
19
+
20
+ // ====================================
21
+ // CLIENT COMPONENTS WITH HOOKS
22
+ // ====================================
23
+
24
+ export function ProductCard({ product }: { product: any }) {
25
+ const analytics = useAnalytics();
26
+ const trackEvent = useTrackEvent();
27
+
28
+ // Method 1: Using the track method with product viewed event
29
+ const handleView = () => {
30
+ if (!analytics) return;
31
+
32
+ void analytics.track('Product Viewed', {
33
+ product_id: product.id,
34
+ name: product.name,
35
+ brand: product.brand,
36
+ category: product.category,
37
+ price: product.price,
38
+ });
39
+ };
40
+
41
+ // Method 2: Using the trackEvent hook
42
+ const handleAddToCart = () => {
43
+ void trackEvent('Product Added to Cart', {
44
+ product_id: product.id,
45
+ name: product.name,
46
+ cart_total: product.price,
47
+ price: product.price,
48
+ quantity: 1,
49
+ });
50
+ };
51
+
52
+ return (
53
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
54
+ <div onMouseEnter={handleView} aria-label={`Product: ${product.name}`}>
55
+ <h3>{product.name}</h3>
56
+ <p>${product.price}</p>
57
+ <button type="button" onClick={handleAddToCart}>
58
+ Add to Cart
59
+ </button>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ // ====================================
65
+ // TRACKED COMPONENTS WITH EMITTERS
66
+ // ====================================
67
+
68
+ export function HeroSection() {
69
+ // Create emitter payloads for tracked components
70
+ const ctaClickEvent = track('CTA Clicked', {
71
+ location: 'hero',
72
+ text: 'Start Free Trial',
73
+ variant: 'primary',
74
+ });
75
+
76
+ const learnMoreEvent = track('Link Clicked', {
77
+ destination: '/features',
78
+ location: 'hero',
79
+ text: 'Learn More',
80
+ });
81
+
82
+ return (
83
+ <section>
84
+ <h1>Welcome to Our Platform</h1>
85
+
86
+ <TrackedButton
87
+ {...(ctaClickEvent.properties && { properties: ctaClickEvent.properties })}
88
+ className="btn-primary"
89
+ eventName={ctaClickEvent.event}
90
+ >
91
+ Start Free Trial
92
+ </TrackedButton>
93
+
94
+ <TrackedLink
95
+ href="/features"
96
+ {...(learnMoreEvent.properties && { properties: learnMoreEvent.properties })}
97
+ eventName={learnMoreEvent.event}
98
+ >
99
+ Learn More →
100
+ </TrackedLink>
101
+ </section>
102
+ );
103
+ }
104
+
105
+ // ====================================
106
+ // FORM TRACKING WITH EMITTERS
107
+ // ====================================
108
+
109
+ export function SignupForm() {
110
+ const analytics = useAnalytics();
111
+ const [formData] = React.useState({
112
+ company: '',
113
+ email: '',
114
+ role: '',
115
+ });
116
+
117
+ // Track form progression with event batch
118
+ const trackFormProgress = React.useCallback(async () => {
119
+ const batch = new EventBatch();
120
+ const filledFields = Object.entries(formData)
121
+ .filter(([_, value]) => value)
122
+ .map(([field]) => field);
123
+
124
+ if (filledFields.length === 1) {
125
+ batch.addTrack('Form Started', {
126
+ form_id: 'signup',
127
+ first_field: filledFields[0],
128
+ });
129
+ }
130
+
131
+ filledFields.forEach(field => {
132
+ batch.addTrack('Form Field Completed', {
133
+ form_id: 'signup',
134
+ field_name: field,
135
+ });
136
+ });
137
+
138
+ if (!analytics) return;
139
+
140
+ // Track each event individually since emitBatch might not be available
141
+ const events = batch.getEvents();
142
+ for (const event of events) {
143
+ if (event.type === 'track') {
144
+ await analytics.track(event.event, event.properties);
145
+ }
146
+ }
147
+ }, [formData, analytics]);
148
+
149
+ React.useEffect(() => {
150
+ void trackFormProgress();
151
+ }, [trackFormProgress]);
152
+
153
+ const handleSubmit = (e: React.FormEvent) => {
154
+ e.preventDefault();
155
+
156
+ // Track form submission with metadata
157
+ const submitEvent = track('Form Submitted', {
158
+ form_id: 'signup',
159
+ fields: Object.keys(formData),
160
+ method: 'manual',
161
+ });
162
+
163
+ if (!analytics) return;
164
+
165
+ void analytics.track(submitEvent.event, {
166
+ ...submitEvent.properties,
167
+ experiment: 'simplified-signup',
168
+ form_version: 'v2',
169
+ });
170
+ };
171
+
172
+ return <form onSubmit={handleSubmit}>{/* Form fields */}</form>;
173
+ }
174
+
175
+ // ====================================
176
+ // SERVER COMPONENTS WITH EMITTERS
177
+ // ====================================
178
+
179
+ export async function ProductListingPage({
180
+ searchParams,
181
+ }: {
182
+ searchParams: { category?: string; sort?: string };
183
+ }) {
184
+ // Track page view in RSC
185
+ await trackServerEvent('Page Viewed', {
186
+ page_name: 'Product Listing',
187
+ category: searchParams.category ?? 'all',
188
+ path: '/products',
189
+ server_rendered: true,
190
+ sort_order: searchParams.sort ?? 'popular',
191
+ });
192
+
193
+ // Track search/filter usage
194
+ if (searchParams.category) {
195
+ await trackServerEvent('Filter Applied', {
196
+ filter_type: 'category',
197
+ filter_value: searchParams.category,
198
+ page: 'products',
199
+ });
200
+ }
201
+
202
+ return <div>{/* Product listing UI */}</div>;
203
+ }
204
+
205
+ // ====================================
206
+ // SERVER ACTIONS WITH EMITTERS
207
+ // ====================================
208
+
209
+ export async function updateUserProfile(formData: FormData) {
210
+ 'use server';
211
+
212
+ const userId = 'user_123'; // Get from session
213
+ const updates = {
214
+ name: formData.get('name') as string,
215
+ company: formData.get('company') as string,
216
+ email: formData.get('email') as string,
217
+ };
218
+
219
+ // Create user session for consistent tracking
220
+ const session = createUserSession(userId, 'session_xyz');
221
+
222
+ // Track profile update
223
+ const updateEvent = session.track('Profile Updated', {
224
+ fields_changed: Object.keys(updates),
225
+ update_source: 'settings_page',
226
+ });
227
+
228
+ await trackEventAction(updateEvent.event, updateEvent.properties);
229
+
230
+ // Update user traits
231
+ const identifyEvent = session.identify(updates);
232
+ await identifyServerUser(identifyEvent.userId, identifyEvent.traits);
233
+
234
+ // Return result
235
+ return { success: true };
236
+ }
237
+
238
+ // ====================================
239
+ // MIDDLEWARE TRACKING WITH EMITTERS
240
+ // ====================================
241
+
242
+ export async function trackMiddlewareEvents(
243
+ request: Request,
244
+ cookiesFn?: () => Promise<{ get: (name: string) => { value: string } | undefined }>,
245
+ ) {
246
+ const url = new URL(request.url);
247
+ // In real usage, cookiesFn should be provided from Next.js app
248
+ // const cookieStore = await cookiesFn?.() || { get: () => undefined };
249
+ const cookieStore = cookiesFn ? await cookiesFn() : { get: () => {} };
250
+ const userId = cookieStore.get('userId')?.value;
251
+
252
+ // Build context for middleware events
253
+ const context = {
254
+ ip: request.headers.get('x-forwarded-for') ?? 'unknown',
255
+ page: {
256
+ url: url.toString(),
257
+ path: url.pathname,
258
+ search: url.search,
259
+ },
260
+ userAgent: request.headers.get('user-agent') ?? 'unknown',
261
+ };
262
+
263
+ // Track page request
264
+ const pageEvent = page(undefined, url.pathname, {
265
+ ...context.page,
266
+ middleware: true,
267
+ });
268
+
269
+ // Add user context if available
270
+ if (userId) {
271
+ return withMetadata(pageEvent, { userId });
272
+ }
273
+
274
+ return pageEvent;
275
+ }
276
+
277
+ // ====================================
278
+ // REAL-TIME FEATURES WITH EMITTERS
279
+ // ====================================
280
+
281
+ export function LiveDashboard() {
282
+ const analytics = useAnalytics();
283
+ const [_metrics, setMetrics] = React.useState({});
284
+
285
+ React.useEffect(() => {
286
+ // Track dashboard view
287
+ if (analytics) {
288
+ void analytics.track('Dashboard Viewed', {
289
+ widgets_visible: 5,
290
+ view_mode: 'real-time',
291
+ });
292
+ }
293
+
294
+ // Set up WebSocket for real-time updates
295
+ const ws = new WebSocket('wss://api.example.com/metrics');
296
+
297
+ ws.onmessage = async event => {
298
+ const data = JSON.parse(event.data);
299
+ setMetrics(data);
300
+
301
+ // Track significant metric changes
302
+ if (data.alert && analytics) {
303
+ await analytics.track('Metric Alert', {
304
+ metric_name: data.alert.metric,
305
+ current_value: data.alert.value,
306
+ severity: data.alert.severity,
307
+ threshold: data.alert.threshold,
308
+ });
309
+ }
310
+ };
311
+
312
+ return () => ws.close();
313
+ }, [analytics]);
314
+
315
+ return <div>{/* Dashboard UI */}</div>;
316
+ }
317
+
318
+ // ====================================
319
+ // ERROR BOUNDARY WITH EMITTERS
320
+ // ====================================
321
+
322
+ export class AnalyticsErrorBoundary extends React.Component<
323
+ { children: React.ReactNode; analytics: any },
324
+ { hasError: boolean }
325
+ > {
326
+ constructor(props: { children: React.ReactNode; analytics: any }) {
327
+ super(props);
328
+ this.state = { hasError: false };
329
+ }
330
+
331
+ static getDerivedStateFromError() {
332
+ return { hasError: true };
333
+ }
334
+
335
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
336
+ // Track error with emitter
337
+ const errorEvent = track('React Error', {
338
+ component_stack: errorInfo.componentStack,
339
+ error_boundary: true,
340
+ error_message: error.message,
341
+ error_stack: error.stack,
342
+ severity: 'high',
343
+ });
344
+
345
+ // Send to analytics
346
+ this.props.analytics.track(errorEvent.event, errorEvent.properties);
347
+ }
348
+
349
+ render() {
350
+ if (this.state.hasError) {
351
+ return <div>Something went wrong.</div>;
352
+ }
353
+
354
+ return this.props.children;
355
+ }
356
+ }
357
+
358
+ // ====================================
359
+ // PROGRESSIVE ENHANCEMENT PATTERN
360
+ // ====================================
361
+
362
+ export function EnhancedSearchBar() {
363
+ const analytics = useAnalytics();
364
+ const [query, setQuery] = React.useState('');
365
+ const [suggestions, _setSuggestions] = React.useState<string[]>([]);
366
+
367
+ // Debounced search tracking
368
+ const timeoutRef = React.useRef<NodeJS.Timeout | undefined>(undefined);
369
+ const trackSearch = React.useMemo(() => {
370
+ return (searchQuery: string) => {
371
+ if (timeoutRef.current) {
372
+ clearTimeout(timeoutRef.current);
373
+ }
374
+ timeoutRef.current = setTimeout(() => {
375
+ if (analytics) {
376
+ void analytics.track('Search Performed', {
377
+ has_suggestions: suggestions.length > 0,
378
+ query: searchQuery,
379
+ query_length: searchQuery.length,
380
+ suggestion_count: suggestions.length,
381
+ });
382
+ }
383
+ }, 500);
384
+ };
385
+ }, [analytics, suggestions.length]);
386
+
387
+ const handleSearch = (value: string) => {
388
+ setQuery(value);
389
+ trackSearch(value);
390
+ // Fetch suggestions...
391
+ };
392
+
393
+ return (
394
+ <div>
395
+ <input
396
+ onChange={e => handleSearch(e.target.value)}
397
+ placeholder="Search..."
398
+ type="search"
399
+ value={query}
400
+ />
401
+ </div>
402
+ );
403
+ }