@feedvalue/react 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.
- package/README.md +224 -0
- package/dist/index.cjs +134 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +173 -0
- package/dist/index.d.ts +173 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# @feedvalue/react
|
|
2
|
+
|
|
3
|
+
React SDK for FeedValue feedback widget. Provides Provider, Hooks, and Components for React 18+.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @feedvalue/react
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @feedvalue/react
|
|
11
|
+
# or
|
|
12
|
+
yarn add @feedvalue/react
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Setup with Provider
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// app/layout.tsx (Next.js App Router)
|
|
21
|
+
import { FeedValueProvider } from '@feedvalue/react';
|
|
22
|
+
|
|
23
|
+
export default function RootLayout({ children }) {
|
|
24
|
+
return (
|
|
25
|
+
<html>
|
|
26
|
+
<body>
|
|
27
|
+
<FeedValueProvider
|
|
28
|
+
widgetId="your-widget-id"
|
|
29
|
+
onReady={() => console.log('Widget ready')}
|
|
30
|
+
onSubmit={(feedback) => console.log('Submitted:', feedback)}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</FeedValueProvider>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Using the Hook
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
'use client';
|
|
44
|
+
|
|
45
|
+
import { useFeedValue } from '@feedvalue/react';
|
|
46
|
+
|
|
47
|
+
export function FeedbackButton() {
|
|
48
|
+
const { open, isReady, isOpen } = useFeedValue();
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<button onClick={open} disabled={!isReady}>
|
|
52
|
+
{isOpen ? 'Close' : 'Give Feedback'}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Using the Widget Component
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
'use client';
|
|
62
|
+
|
|
63
|
+
import { FeedValueWidget } from '@feedvalue/react';
|
|
64
|
+
|
|
65
|
+
// Standalone widget (no Provider needed)
|
|
66
|
+
export function FeedbackWidget() {
|
|
67
|
+
return (
|
|
68
|
+
<FeedValueWidget
|
|
69
|
+
widgetId="your-widget-id"
|
|
70
|
+
onSubmit={(feedback) => console.log('Submitted:', feedback)}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Programmatic Control
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
'use client';
|
|
80
|
+
|
|
81
|
+
import { useFeedValue } from '@feedvalue/react';
|
|
82
|
+
|
|
83
|
+
export function FeedbackForm() {
|
|
84
|
+
const { submit, isSubmitting, error } = useFeedValue();
|
|
85
|
+
|
|
86
|
+
const handleSubmit = async (message: string) => {
|
|
87
|
+
try {
|
|
88
|
+
await submit({ message });
|
|
89
|
+
console.log('Feedback submitted successfully');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('Failed to submit:', err);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<form onSubmit={(e) => {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
handleSubmit(e.currentTarget.message.value);
|
|
99
|
+
}}>
|
|
100
|
+
<textarea name="message" placeholder="Your feedback..." />
|
|
101
|
+
<button type="submit" disabled={isSubmitting}>
|
|
102
|
+
{isSubmitting ? 'Submitting...' : 'Submit'}
|
|
103
|
+
</button>
|
|
104
|
+
{error && <p className="error">{error.message}</p>}
|
|
105
|
+
</form>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### User Identification
|
|
111
|
+
|
|
112
|
+
```tsx
|
|
113
|
+
'use client';
|
|
114
|
+
|
|
115
|
+
import { useFeedValue } from '@feedvalue/react';
|
|
116
|
+
import { useEffect } from 'react';
|
|
117
|
+
|
|
118
|
+
export function UserIdentifier({ user }) {
|
|
119
|
+
const { identify, setData, reset } = useFeedValue();
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (user) {
|
|
123
|
+
identify(user.id, {
|
|
124
|
+
name: user.name,
|
|
125
|
+
email: user.email,
|
|
126
|
+
plan: user.plan,
|
|
127
|
+
});
|
|
128
|
+
setData({ company: user.company });
|
|
129
|
+
} else {
|
|
130
|
+
reset();
|
|
131
|
+
}
|
|
132
|
+
}, [user, identify, setData, reset]);
|
|
133
|
+
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## API Reference
|
|
139
|
+
|
|
140
|
+
### `<FeedValueProvider>`
|
|
141
|
+
|
|
142
|
+
Provider component for FeedValue context.
|
|
143
|
+
|
|
144
|
+
| Prop | Type | Required | Description |
|
|
145
|
+
|------|------|----------|-------------|
|
|
146
|
+
| `widgetId` | `string` | Yes | Widget ID from FeedValue dashboard |
|
|
147
|
+
| `apiBaseUrl` | `string` | No | Custom API URL (for self-hosted) |
|
|
148
|
+
| `config` | `Partial<FeedValueConfig>` | No | Configuration overrides |
|
|
149
|
+
| `onReady` | `() => void` | No | Called when widget is ready |
|
|
150
|
+
| `onOpen` | `() => void` | No | Called when modal opens |
|
|
151
|
+
| `onClose` | `() => void` | No | Called when modal closes |
|
|
152
|
+
| `onSubmit` | `(feedback: FeedbackData) => void` | No | Called when feedback is submitted |
|
|
153
|
+
| `onError` | `(error: Error) => void` | No | Called on errors |
|
|
154
|
+
|
|
155
|
+
### `useFeedValue()`
|
|
156
|
+
|
|
157
|
+
Hook to access FeedValue context. Must be used within `<FeedValueProvider>`.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
|
|
161
|
+
| Property | Type | Description |
|
|
162
|
+
|----------|------|-------------|
|
|
163
|
+
| `isReady` | `boolean` | Widget is initialized |
|
|
164
|
+
| `isOpen` | `boolean` | Modal is open |
|
|
165
|
+
| `isVisible` | `boolean` | Trigger button is visible |
|
|
166
|
+
| `error` | `Error \| null` | Current error |
|
|
167
|
+
| `isSubmitting` | `boolean` | Submission in progress |
|
|
168
|
+
| `open` | `() => void` | Open the modal |
|
|
169
|
+
| `close` | `() => void` | Close the modal |
|
|
170
|
+
| `toggle` | `() => void` | Toggle modal |
|
|
171
|
+
| `show` | `() => void` | Show trigger |
|
|
172
|
+
| `hide` | `() => void` | Hide trigger |
|
|
173
|
+
| `submit` | `(feedback) => Promise<void>` | Submit feedback |
|
|
174
|
+
| `identify` | `(userId, traits?) => void` | Identify user |
|
|
175
|
+
| `setData` | `(data) => void` | Set user data |
|
|
176
|
+
| `reset` | `() => void` | Reset user data |
|
|
177
|
+
|
|
178
|
+
### `useFeedValueOptional()`
|
|
179
|
+
|
|
180
|
+
Same as `useFeedValue()` but returns `null` if used outside provider instead of throwing.
|
|
181
|
+
|
|
182
|
+
### `<FeedValueWidget>`
|
|
183
|
+
|
|
184
|
+
Standalone widget component that doesn't require a provider.
|
|
185
|
+
|
|
186
|
+
| Prop | Type | Required | Description |
|
|
187
|
+
|------|------|----------|-------------|
|
|
188
|
+
| `widgetId` | `string` | Yes | Widget ID from FeedValue dashboard |
|
|
189
|
+
| `apiBaseUrl` | `string` | No | Custom API URL |
|
|
190
|
+
| `config` | `Partial<FeedValueConfig>` | No | Configuration overrides |
|
|
191
|
+
| `onReady` | `() => void` | No | Ready callback |
|
|
192
|
+
| `onSubmit` | `(feedback) => void` | No | Submit callback |
|
|
193
|
+
| `onError` | `(error) => void` | No | Error callback |
|
|
194
|
+
|
|
195
|
+
## Server-Side Rendering
|
|
196
|
+
|
|
197
|
+
The SDK is fully SSR-compatible. It uses `useSyncExternalStore` for concurrent mode support and returns safe default values during server rendering.
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// Works out of the box with Next.js App Router
|
|
201
|
+
'use client';
|
|
202
|
+
|
|
203
|
+
import { useFeedValue } from '@feedvalue/react';
|
|
204
|
+
|
|
205
|
+
export function FeedbackButton() {
|
|
206
|
+
const { open, isReady } = useFeedValue();
|
|
207
|
+
|
|
208
|
+
// isReady is false during SSR, preventing hydration mismatches
|
|
209
|
+
return (
|
|
210
|
+
<button onClick={open} disabled={!isReady}>
|
|
211
|
+
Feedback
|
|
212
|
+
</button>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Requirements
|
|
218
|
+
|
|
219
|
+
- React 18.0.0 or higher
|
|
220
|
+
- React DOM 18.0.0 or higher
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var core = require('@feedvalue/core');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
|
|
7
|
+
var FeedValueContext = react.createContext(null);
|
|
8
|
+
var getServerSnapshot = () => ({
|
|
9
|
+
isReady: false,
|
|
10
|
+
isOpen: false,
|
|
11
|
+
isVisible: false,
|
|
12
|
+
error: null,
|
|
13
|
+
isSubmitting: false
|
|
14
|
+
});
|
|
15
|
+
function FeedValueProvider({
|
|
16
|
+
widgetId,
|
|
17
|
+
apiBaseUrl,
|
|
18
|
+
config,
|
|
19
|
+
headless,
|
|
20
|
+
children,
|
|
21
|
+
onReady,
|
|
22
|
+
onOpen,
|
|
23
|
+
onClose,
|
|
24
|
+
onSubmit,
|
|
25
|
+
onError
|
|
26
|
+
}) {
|
|
27
|
+
const instanceRef = react.useRef(null);
|
|
28
|
+
const callbacksRef = react.useRef({ onReady, onOpen, onClose, onSubmit, onError });
|
|
29
|
+
callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };
|
|
30
|
+
react.useEffect(() => {
|
|
31
|
+
if (typeof window === "undefined") return;
|
|
32
|
+
const instance = core.FeedValue.init({
|
|
33
|
+
widgetId,
|
|
34
|
+
apiBaseUrl,
|
|
35
|
+
config,
|
|
36
|
+
headless
|
|
37
|
+
});
|
|
38
|
+
instanceRef.current = instance;
|
|
39
|
+
const handleReady = () => callbacksRef.current.onReady?.();
|
|
40
|
+
const handleOpen = () => callbacksRef.current.onOpen?.();
|
|
41
|
+
const handleClose = () => callbacksRef.current.onClose?.();
|
|
42
|
+
const handleSubmit = (feedback) => callbacksRef.current.onSubmit?.(feedback);
|
|
43
|
+
const handleError = (error) => callbacksRef.current.onError?.(error);
|
|
44
|
+
instance.on("ready", handleReady);
|
|
45
|
+
instance.on("open", handleOpen);
|
|
46
|
+
instance.on("close", handleClose);
|
|
47
|
+
instance.on("submit", handleSubmit);
|
|
48
|
+
instance.on("error", handleError);
|
|
49
|
+
return () => {
|
|
50
|
+
instance.off("ready", handleReady);
|
|
51
|
+
instance.off("open", handleOpen);
|
|
52
|
+
instance.off("close", handleClose);
|
|
53
|
+
instance.off("submit", handleSubmit);
|
|
54
|
+
instance.off("error", handleError);
|
|
55
|
+
instance.destroy();
|
|
56
|
+
instanceRef.current = null;
|
|
57
|
+
};
|
|
58
|
+
}, [widgetId, apiBaseUrl, headless]);
|
|
59
|
+
const state = react.useSyncExternalStore(
|
|
60
|
+
// Subscribe function
|
|
61
|
+
(callback) => {
|
|
62
|
+
const instance = instanceRef.current;
|
|
63
|
+
if (!instance) return () => {
|
|
64
|
+
};
|
|
65
|
+
return instance.subscribe(callback);
|
|
66
|
+
},
|
|
67
|
+
// getSnapshot - client
|
|
68
|
+
() => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),
|
|
69
|
+
// getServerSnapshot - SSR
|
|
70
|
+
getServerSnapshot
|
|
71
|
+
);
|
|
72
|
+
const open = react.useCallback(() => instanceRef.current?.open(), []);
|
|
73
|
+
const close = react.useCallback(() => instanceRef.current?.close(), []);
|
|
74
|
+
const toggle = react.useCallback(() => instanceRef.current?.toggle(), []);
|
|
75
|
+
const show = react.useCallback(() => instanceRef.current?.show(), []);
|
|
76
|
+
const hide = react.useCallback(() => instanceRef.current?.hide(), []);
|
|
77
|
+
const submit = react.useCallback(
|
|
78
|
+
(feedback) => instanceRef.current?.submit(feedback) ?? Promise.reject(new Error("Not initialized")),
|
|
79
|
+
[]
|
|
80
|
+
);
|
|
81
|
+
const identify = react.useCallback(
|
|
82
|
+
(userId, traits) => instanceRef.current?.identify(userId, traits),
|
|
83
|
+
[]
|
|
84
|
+
);
|
|
85
|
+
const setData = react.useCallback(
|
|
86
|
+
(data) => instanceRef.current?.setData(data),
|
|
87
|
+
[]
|
|
88
|
+
);
|
|
89
|
+
const reset = react.useCallback(() => instanceRef.current?.reset(), []);
|
|
90
|
+
const value = react.useMemo(
|
|
91
|
+
() => ({
|
|
92
|
+
instance: instanceRef.current,
|
|
93
|
+
isReady: state.isReady,
|
|
94
|
+
isOpen: state.isOpen,
|
|
95
|
+
isVisible: state.isVisible,
|
|
96
|
+
error: state.error,
|
|
97
|
+
isSubmitting: state.isSubmitting,
|
|
98
|
+
isHeadless: headless ?? false,
|
|
99
|
+
open,
|
|
100
|
+
close,
|
|
101
|
+
toggle,
|
|
102
|
+
show,
|
|
103
|
+
hide,
|
|
104
|
+
submit,
|
|
105
|
+
identify,
|
|
106
|
+
setData,
|
|
107
|
+
reset
|
|
108
|
+
}),
|
|
109
|
+
[state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]
|
|
110
|
+
);
|
|
111
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FeedValueContext.Provider, { value, children });
|
|
112
|
+
}
|
|
113
|
+
function useFeedValue() {
|
|
114
|
+
const context = react.useContext(FeedValueContext);
|
|
115
|
+
if (!context) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'useFeedValue must be used within a FeedValueProvider. Make sure to wrap your app with <FeedValueProvider widgetId="...">.'
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
return context;
|
|
121
|
+
}
|
|
122
|
+
function useFeedValueOptional() {
|
|
123
|
+
return react.useContext(FeedValueContext);
|
|
124
|
+
}
|
|
125
|
+
function FeedValueWidget(props) {
|
|
126
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FeedValueProvider, { ...props, children: null });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
exports.FeedValueProvider = FeedValueProvider;
|
|
130
|
+
exports.FeedValueWidget = FeedValueWidget;
|
|
131
|
+
exports.useFeedValue = useFeedValue;
|
|
132
|
+
exports.useFeedValueOptional = useFeedValueOptional;
|
|
133
|
+
//# sourceMappingURL=index.cjs.map
|
|
134
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/components.tsx"],"names":["createContext","useRef","useEffect","FeedValue","useSyncExternalStore","useCallback","useMemo","jsx","useContext"],"mappings":"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","file":"index.cjs","sourcesContent":["'use client';\n\n/**\n * @feedvalue/react - Provider\n *\n * FeedValueProvider component for React applications.\n * Uses useSyncExternalStore for React 18+ concurrent mode compatibility.\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useSyncExternalStore,\n useMemo,\n type ReactNode,\n} from 'react';\nimport {\n FeedValue,\n type FeedValueConfig,\n type FeedValueState,\n type FeedbackData,\n type UserTraits,\n} from '@feedvalue/core';\n\n/**\n * Context value provided by FeedValueProvider\n */\nexport interface FeedValueContextValue {\n /** FeedValue instance (for advanced usage) */\n instance: FeedValue | null;\n /** Widget is ready (config loaded) */\n isReady: boolean;\n /** Modal is open */\n isOpen: boolean;\n /** Widget is visible */\n isVisible: boolean;\n /** Current error (if any) */\n error: Error | null;\n /** Submission in progress */\n isSubmitting: boolean;\n /** Running in headless mode (no default UI rendered) */\n isHeadless: boolean;\n /** Open the feedback modal */\n open: () => void;\n /** Close the feedback modal */\n close: () => void;\n /** Toggle the feedback modal */\n toggle: () => void;\n /** Show the trigger button */\n show: () => void;\n /** Hide the trigger button */\n hide: () => void;\n /** Submit feedback programmatically */\n submit: (feedback: Partial<FeedbackData>) => Promise<void>;\n /** Identify user */\n identify: (userId: string, traits?: UserTraits) => void;\n /** Set user data */\n setData: (data: Record<string, string>) => void;\n /** Reset user data */\n reset: () => void;\n}\n\n/**\n * FeedValue context (internal)\n */\nconst FeedValueContext = createContext<FeedValueContextValue | null>(null);\n\n/**\n * Props for FeedValueProvider\n */\nexport interface FeedValueProviderProps {\n /** Widget ID from FeedValue dashboard */\n widgetId: string;\n /** API base URL (for self-hosted) */\n apiBaseUrl?: string;\n /** Configuration overrides */\n config?: Partial<FeedValueConfig>;\n /**\n * Headless mode - disables all DOM rendering.\n * Use this when you want full control over the UI.\n * The SDK will still fetch config and provide all API methods\n * but won't render any trigger button or modal.\n *\n * @default false\n */\n headless?: boolean;\n /** Children */\n children: ReactNode;\n /** Callback when widget is ready */\n onReady?: () => void;\n /** Callback when modal opens */\n onOpen?: () => void;\n /** Callback when modal closes */\n onClose?: () => void;\n /** Callback when feedback is submitted */\n onSubmit?: (feedback: FeedbackData) => void;\n /** Callback when an error occurs */\n onError?: (error: Error) => void;\n}\n\n/**\n * Server snapshot for SSR - always returns initial state\n * This prevents hydration mismatches\n */\nconst getServerSnapshot = (): FeedValueState => ({\n isReady: false,\n isOpen: false,\n isVisible: false,\n error: null,\n isSubmitting: false,\n});\n\n/**\n * FeedValueProvider component\n *\n * Provides FeedValue context to child components.\n *\n * @example\n * ```tsx\n * // app/layout.tsx (Next.js App Router)\n * import { FeedValueProvider } from '@feedvalue/react';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * <FeedValueProvider\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Submitted:', feedback)}\n * >\n * {children}\n * </FeedValueProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function FeedValueProvider({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n children,\n onReady,\n onOpen,\n onClose,\n onSubmit,\n onError,\n}: FeedValueProviderProps): React.ReactElement {\n const instanceRef = useRef<FeedValue | null>(null);\n const callbacksRef = useRef({ onReady, onOpen, onClose, onSubmit, onError });\n\n // Keep callbacks ref updated\n callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };\n\n // Initialize on mount\n useEffect(() => {\n // Only initialize on client side\n if (typeof window === 'undefined') return;\n\n const instance = FeedValue.init({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n });\n instanceRef.current = instance;\n\n // Subscribe to events\n const handleReady = () => callbacksRef.current.onReady?.();\n const handleOpen = () => callbacksRef.current.onOpen?.();\n const handleClose = () => callbacksRef.current.onClose?.();\n const handleSubmit = (feedback: FeedbackData) => callbacksRef.current.onSubmit?.(feedback);\n const handleError = (error: Error) => callbacksRef.current.onError?.(error);\n\n instance.on('ready', handleReady);\n instance.on('open', handleOpen);\n instance.on('close', handleClose);\n instance.on('submit', handleSubmit);\n instance.on('error', handleError);\n\n return () => {\n instance.off('ready', handleReady);\n instance.off('open', handleOpen);\n instance.off('close', handleClose);\n instance.off('submit', handleSubmit);\n instance.off('error', handleError);\n instance.destroy();\n instanceRef.current = null;\n };\n }, [widgetId, apiBaseUrl, headless]); // Re-init if widgetId, apiBaseUrl, or headless changes\n\n // Use useSyncExternalStore for concurrent mode compatibility\n const state = useSyncExternalStore(\n // Subscribe function\n (callback) => {\n const instance = instanceRef.current;\n if (!instance) return () => {};\n return instance.subscribe(callback);\n },\n // getSnapshot - client\n () => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),\n // getServerSnapshot - SSR\n getServerSnapshot\n );\n\n // Stable callback references to prevent recreation on every state change\n const open = useCallback(() => instanceRef.current?.open(), []);\n const close = useCallback(() => instanceRef.current?.close(), []);\n const toggle = useCallback(() => instanceRef.current?.toggle(), []);\n const show = useCallback(() => instanceRef.current?.show(), []);\n const hide = useCallback(() => instanceRef.current?.hide(), []);\n const submit = useCallback(\n (feedback: Partial<FeedbackData>) =>\n instanceRef.current?.submit(feedback) ?? Promise.reject(new Error('Not initialized')),\n []\n );\n const identify = useCallback(\n (userId: string, traits?: UserTraits) => instanceRef.current?.identify(userId, traits),\n []\n );\n const setData = useCallback(\n (data: Record<string, string>) => instanceRef.current?.setData(data),\n []\n );\n const reset = useCallback(() => instanceRef.current?.reset(), []);\n\n // Memoize context value to prevent unnecessary re-renders\n const value = useMemo<FeedValueContextValue>(\n () => ({\n instance: instanceRef.current,\n isReady: state.isReady,\n isOpen: state.isOpen,\n isVisible: state.isVisible,\n error: state.error,\n isSubmitting: state.isSubmitting,\n isHeadless: headless ?? false,\n open,\n close,\n toggle,\n show,\n hide,\n submit,\n identify,\n setData,\n reset,\n }),\n [state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]\n );\n\n return (\n <FeedValueContext.Provider value={value}>\n {children}\n </FeedValueContext.Provider>\n );\n}\n\n/**\n * Hook to access FeedValue context\n *\n * Must be used within a FeedValueProvider.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useFeedValue } from '@feedvalue/react';\n *\n * export function FeedbackButton() {\n * const { open, isReady } = useFeedValue();\n *\n * return (\n * <button onClick={open} disabled={!isReady}>\n * Give Feedback\n * </button>\n * );\n * }\n * ```\n */\nexport function useFeedValue(): FeedValueContextValue {\n const context = useContext(FeedValueContext);\n\n if (!context) {\n throw new Error(\n 'useFeedValue must be used within a FeedValueProvider. ' +\n 'Make sure to wrap your app with <FeedValueProvider widgetId=\"...\">.'\n );\n }\n\n return context;\n}\n\n/**\n * Hook to check if inside FeedValueProvider\n * Returns null if outside provider, context value if inside\n */\nexport function useFeedValueOptional(): FeedValueContextValue | null {\n return useContext(FeedValueContext);\n}\n","'use client';\n\n/**\n * @feedvalue/react - Components\n *\n * Standalone React components for FeedValue.\n */\n\nimport React from 'react';\nimport { FeedValueProvider, type FeedValueProviderProps } from './provider';\n\n/**\n * Props for FeedValueWidget component\n */\nexport interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {}\n\n/**\n * Standalone FeedValue widget component\n *\n * Use this when you don't need to access the FeedValue context elsewhere.\n * The widget renders itself via DOM injection - this component is a container.\n *\n * @example\n * ```tsx\n * // Simple usage - just drop in anywhere\n * import { FeedValueWidget } from '@feedvalue/react';\n *\n * export function App() {\n * return (\n * <div>\n * <h1>My App</h1>\n * <FeedValueWidget\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Feedback:', feedback)}\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement {\n return (\n <FeedValueProvider {...props}>\n {/* Widget renders via DOM injection, no children needed */}\n {null}\n </FeedValueProvider>\n );\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { FeedValue, FeedbackData, UserTraits, FeedValueConfig } from '@feedvalue/core';
|
|
3
|
+
export { FeedValueConfig, FeedValueEvents, FeedValueState, FeedbackData, FeedbackMetadata, UserData, UserTraits } from '@feedvalue/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @feedvalue/react - Provider
|
|
7
|
+
*
|
|
8
|
+
* FeedValueProvider component for React applications.
|
|
9
|
+
* Uses useSyncExternalStore for React 18+ concurrent mode compatibility.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context value provided by FeedValueProvider
|
|
14
|
+
*/
|
|
15
|
+
interface FeedValueContextValue {
|
|
16
|
+
/** FeedValue instance (for advanced usage) */
|
|
17
|
+
instance: FeedValue | null;
|
|
18
|
+
/** Widget is ready (config loaded) */
|
|
19
|
+
isReady: boolean;
|
|
20
|
+
/** Modal is open */
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
/** Widget is visible */
|
|
23
|
+
isVisible: boolean;
|
|
24
|
+
/** Current error (if any) */
|
|
25
|
+
error: Error | null;
|
|
26
|
+
/** Submission in progress */
|
|
27
|
+
isSubmitting: boolean;
|
|
28
|
+
/** Running in headless mode (no default UI rendered) */
|
|
29
|
+
isHeadless: boolean;
|
|
30
|
+
/** Open the feedback modal */
|
|
31
|
+
open: () => void;
|
|
32
|
+
/** Close the feedback modal */
|
|
33
|
+
close: () => void;
|
|
34
|
+
/** Toggle the feedback modal */
|
|
35
|
+
toggle: () => void;
|
|
36
|
+
/** Show the trigger button */
|
|
37
|
+
show: () => void;
|
|
38
|
+
/** Hide the trigger button */
|
|
39
|
+
hide: () => void;
|
|
40
|
+
/** Submit feedback programmatically */
|
|
41
|
+
submit: (feedback: Partial<FeedbackData>) => Promise<void>;
|
|
42
|
+
/** Identify user */
|
|
43
|
+
identify: (userId: string, traits?: UserTraits) => void;
|
|
44
|
+
/** Set user data */
|
|
45
|
+
setData: (data: Record<string, string>) => void;
|
|
46
|
+
/** Reset user data */
|
|
47
|
+
reset: () => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Props for FeedValueProvider
|
|
51
|
+
*/
|
|
52
|
+
interface FeedValueProviderProps {
|
|
53
|
+
/** Widget ID from FeedValue dashboard */
|
|
54
|
+
widgetId: string;
|
|
55
|
+
/** API base URL (for self-hosted) */
|
|
56
|
+
apiBaseUrl?: string;
|
|
57
|
+
/** Configuration overrides */
|
|
58
|
+
config?: Partial<FeedValueConfig>;
|
|
59
|
+
/**
|
|
60
|
+
* Headless mode - disables all DOM rendering.
|
|
61
|
+
* Use this when you want full control over the UI.
|
|
62
|
+
* The SDK will still fetch config and provide all API methods
|
|
63
|
+
* but won't render any trigger button or modal.
|
|
64
|
+
*
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
headless?: boolean;
|
|
68
|
+
/** Children */
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
/** Callback when widget is ready */
|
|
71
|
+
onReady?: () => void;
|
|
72
|
+
/** Callback when modal opens */
|
|
73
|
+
onOpen?: () => void;
|
|
74
|
+
/** Callback when modal closes */
|
|
75
|
+
onClose?: () => void;
|
|
76
|
+
/** Callback when feedback is submitted */
|
|
77
|
+
onSubmit?: (feedback: FeedbackData) => void;
|
|
78
|
+
/** Callback when an error occurs */
|
|
79
|
+
onError?: (error: Error) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* FeedValueProvider component
|
|
83
|
+
*
|
|
84
|
+
* Provides FeedValue context to child components.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* // app/layout.tsx (Next.js App Router)
|
|
89
|
+
* import { FeedValueProvider } from '@feedvalue/react';
|
|
90
|
+
*
|
|
91
|
+
* export default function RootLayout({ children }) {
|
|
92
|
+
* return (
|
|
93
|
+
* <html>
|
|
94
|
+
* <body>
|
|
95
|
+
* <FeedValueProvider
|
|
96
|
+
* widgetId="your-widget-id"
|
|
97
|
+
* onSubmit={(feedback) => console.log('Submitted:', feedback)}
|
|
98
|
+
* >
|
|
99
|
+
* {children}
|
|
100
|
+
* </FeedValueProvider>
|
|
101
|
+
* </body>
|
|
102
|
+
* </html>
|
|
103
|
+
* );
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
declare function FeedValueProvider({ widgetId, apiBaseUrl, config, headless, children, onReady, onOpen, onClose, onSubmit, onError, }: FeedValueProviderProps): React.ReactElement;
|
|
108
|
+
/**
|
|
109
|
+
* Hook to access FeedValue context
|
|
110
|
+
*
|
|
111
|
+
* Must be used within a FeedValueProvider.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```tsx
|
|
115
|
+
* 'use client';
|
|
116
|
+
* import { useFeedValue } from '@feedvalue/react';
|
|
117
|
+
*
|
|
118
|
+
* export function FeedbackButton() {
|
|
119
|
+
* const { open, isReady } = useFeedValue();
|
|
120
|
+
*
|
|
121
|
+
* return (
|
|
122
|
+
* <button onClick={open} disabled={!isReady}>
|
|
123
|
+
* Give Feedback
|
|
124
|
+
* </button>
|
|
125
|
+
* );
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function useFeedValue(): FeedValueContextValue;
|
|
130
|
+
/**
|
|
131
|
+
* Hook to check if inside FeedValueProvider
|
|
132
|
+
* Returns null if outside provider, context value if inside
|
|
133
|
+
*/
|
|
134
|
+
declare function useFeedValueOptional(): FeedValueContextValue | null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @feedvalue/react - Components
|
|
138
|
+
*
|
|
139
|
+
* Standalone React components for FeedValue.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Props for FeedValueWidget component
|
|
144
|
+
*/
|
|
145
|
+
interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Standalone FeedValue widget component
|
|
149
|
+
*
|
|
150
|
+
* Use this when you don't need to access the FeedValue context elsewhere.
|
|
151
|
+
* The widget renders itself via DOM injection - this component is a container.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* // Simple usage - just drop in anywhere
|
|
156
|
+
* import { FeedValueWidget } from '@feedvalue/react';
|
|
157
|
+
*
|
|
158
|
+
* export function App() {
|
|
159
|
+
* return (
|
|
160
|
+
* <div>
|
|
161
|
+
* <h1>My App</h1>
|
|
162
|
+
* <FeedValueWidget
|
|
163
|
+
* widgetId="your-widget-id"
|
|
164
|
+
* onSubmit={(feedback) => console.log('Feedback:', feedback)}
|
|
165
|
+
* />
|
|
166
|
+
* </div>
|
|
167
|
+
* );
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement;
|
|
172
|
+
|
|
173
|
+
export { type FeedValueContextValue, FeedValueProvider, type FeedValueProviderProps, FeedValueWidget, type FeedValueWidgetProps, useFeedValue, useFeedValueOptional };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { FeedValue, FeedbackData, UserTraits, FeedValueConfig } from '@feedvalue/core';
|
|
3
|
+
export { FeedValueConfig, FeedValueEvents, FeedValueState, FeedbackData, FeedbackMetadata, UserData, UserTraits } from '@feedvalue/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @feedvalue/react - Provider
|
|
7
|
+
*
|
|
8
|
+
* FeedValueProvider component for React applications.
|
|
9
|
+
* Uses useSyncExternalStore for React 18+ concurrent mode compatibility.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Context value provided by FeedValueProvider
|
|
14
|
+
*/
|
|
15
|
+
interface FeedValueContextValue {
|
|
16
|
+
/** FeedValue instance (for advanced usage) */
|
|
17
|
+
instance: FeedValue | null;
|
|
18
|
+
/** Widget is ready (config loaded) */
|
|
19
|
+
isReady: boolean;
|
|
20
|
+
/** Modal is open */
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
/** Widget is visible */
|
|
23
|
+
isVisible: boolean;
|
|
24
|
+
/** Current error (if any) */
|
|
25
|
+
error: Error | null;
|
|
26
|
+
/** Submission in progress */
|
|
27
|
+
isSubmitting: boolean;
|
|
28
|
+
/** Running in headless mode (no default UI rendered) */
|
|
29
|
+
isHeadless: boolean;
|
|
30
|
+
/** Open the feedback modal */
|
|
31
|
+
open: () => void;
|
|
32
|
+
/** Close the feedback modal */
|
|
33
|
+
close: () => void;
|
|
34
|
+
/** Toggle the feedback modal */
|
|
35
|
+
toggle: () => void;
|
|
36
|
+
/** Show the trigger button */
|
|
37
|
+
show: () => void;
|
|
38
|
+
/** Hide the trigger button */
|
|
39
|
+
hide: () => void;
|
|
40
|
+
/** Submit feedback programmatically */
|
|
41
|
+
submit: (feedback: Partial<FeedbackData>) => Promise<void>;
|
|
42
|
+
/** Identify user */
|
|
43
|
+
identify: (userId: string, traits?: UserTraits) => void;
|
|
44
|
+
/** Set user data */
|
|
45
|
+
setData: (data: Record<string, string>) => void;
|
|
46
|
+
/** Reset user data */
|
|
47
|
+
reset: () => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Props for FeedValueProvider
|
|
51
|
+
*/
|
|
52
|
+
interface FeedValueProviderProps {
|
|
53
|
+
/** Widget ID from FeedValue dashboard */
|
|
54
|
+
widgetId: string;
|
|
55
|
+
/** API base URL (for self-hosted) */
|
|
56
|
+
apiBaseUrl?: string;
|
|
57
|
+
/** Configuration overrides */
|
|
58
|
+
config?: Partial<FeedValueConfig>;
|
|
59
|
+
/**
|
|
60
|
+
* Headless mode - disables all DOM rendering.
|
|
61
|
+
* Use this when you want full control over the UI.
|
|
62
|
+
* The SDK will still fetch config and provide all API methods
|
|
63
|
+
* but won't render any trigger button or modal.
|
|
64
|
+
*
|
|
65
|
+
* @default false
|
|
66
|
+
*/
|
|
67
|
+
headless?: boolean;
|
|
68
|
+
/** Children */
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
/** Callback when widget is ready */
|
|
71
|
+
onReady?: () => void;
|
|
72
|
+
/** Callback when modal opens */
|
|
73
|
+
onOpen?: () => void;
|
|
74
|
+
/** Callback when modal closes */
|
|
75
|
+
onClose?: () => void;
|
|
76
|
+
/** Callback when feedback is submitted */
|
|
77
|
+
onSubmit?: (feedback: FeedbackData) => void;
|
|
78
|
+
/** Callback when an error occurs */
|
|
79
|
+
onError?: (error: Error) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* FeedValueProvider component
|
|
83
|
+
*
|
|
84
|
+
* Provides FeedValue context to child components.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```tsx
|
|
88
|
+
* // app/layout.tsx (Next.js App Router)
|
|
89
|
+
* import { FeedValueProvider } from '@feedvalue/react';
|
|
90
|
+
*
|
|
91
|
+
* export default function RootLayout({ children }) {
|
|
92
|
+
* return (
|
|
93
|
+
* <html>
|
|
94
|
+
* <body>
|
|
95
|
+
* <FeedValueProvider
|
|
96
|
+
* widgetId="your-widget-id"
|
|
97
|
+
* onSubmit={(feedback) => console.log('Submitted:', feedback)}
|
|
98
|
+
* >
|
|
99
|
+
* {children}
|
|
100
|
+
* </FeedValueProvider>
|
|
101
|
+
* </body>
|
|
102
|
+
* </html>
|
|
103
|
+
* );
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
declare function FeedValueProvider({ widgetId, apiBaseUrl, config, headless, children, onReady, onOpen, onClose, onSubmit, onError, }: FeedValueProviderProps): React.ReactElement;
|
|
108
|
+
/**
|
|
109
|
+
* Hook to access FeedValue context
|
|
110
|
+
*
|
|
111
|
+
* Must be used within a FeedValueProvider.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```tsx
|
|
115
|
+
* 'use client';
|
|
116
|
+
* import { useFeedValue } from '@feedvalue/react';
|
|
117
|
+
*
|
|
118
|
+
* export function FeedbackButton() {
|
|
119
|
+
* const { open, isReady } = useFeedValue();
|
|
120
|
+
*
|
|
121
|
+
* return (
|
|
122
|
+
* <button onClick={open} disabled={!isReady}>
|
|
123
|
+
* Give Feedback
|
|
124
|
+
* </button>
|
|
125
|
+
* );
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare function useFeedValue(): FeedValueContextValue;
|
|
130
|
+
/**
|
|
131
|
+
* Hook to check if inside FeedValueProvider
|
|
132
|
+
* Returns null if outside provider, context value if inside
|
|
133
|
+
*/
|
|
134
|
+
declare function useFeedValueOptional(): FeedValueContextValue | null;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @feedvalue/react - Components
|
|
138
|
+
*
|
|
139
|
+
* Standalone React components for FeedValue.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Props for FeedValueWidget component
|
|
144
|
+
*/
|
|
145
|
+
interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Standalone FeedValue widget component
|
|
149
|
+
*
|
|
150
|
+
* Use this when you don't need to access the FeedValue context elsewhere.
|
|
151
|
+
* The widget renders itself via DOM injection - this component is a container.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* // Simple usage - just drop in anywhere
|
|
156
|
+
* import { FeedValueWidget } from '@feedvalue/react';
|
|
157
|
+
*
|
|
158
|
+
* export function App() {
|
|
159
|
+
* return (
|
|
160
|
+
* <div>
|
|
161
|
+
* <h1>My App</h1>
|
|
162
|
+
* <FeedValueWidget
|
|
163
|
+
* widgetId="your-widget-id"
|
|
164
|
+
* onSubmit={(feedback) => console.log('Feedback:', feedback)}
|
|
165
|
+
* />
|
|
166
|
+
* </div>
|
|
167
|
+
* );
|
|
168
|
+
* }
|
|
169
|
+
* ```
|
|
170
|
+
*/
|
|
171
|
+
declare function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement;
|
|
172
|
+
|
|
173
|
+
export { type FeedValueContextValue, FeedValueProvider, type FeedValueProviderProps, FeedValueWidget, type FeedValueWidgetProps, useFeedValue, useFeedValueOptional };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createContext, useRef, useEffect, useSyncExternalStore, useCallback, useMemo, useContext } from 'react';
|
|
2
|
+
import { FeedValue } from '@feedvalue/core';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var FeedValueContext = createContext(null);
|
|
6
|
+
var getServerSnapshot = () => ({
|
|
7
|
+
isReady: false,
|
|
8
|
+
isOpen: false,
|
|
9
|
+
isVisible: false,
|
|
10
|
+
error: null,
|
|
11
|
+
isSubmitting: false
|
|
12
|
+
});
|
|
13
|
+
function FeedValueProvider({
|
|
14
|
+
widgetId,
|
|
15
|
+
apiBaseUrl,
|
|
16
|
+
config,
|
|
17
|
+
headless,
|
|
18
|
+
children,
|
|
19
|
+
onReady,
|
|
20
|
+
onOpen,
|
|
21
|
+
onClose,
|
|
22
|
+
onSubmit,
|
|
23
|
+
onError
|
|
24
|
+
}) {
|
|
25
|
+
const instanceRef = useRef(null);
|
|
26
|
+
const callbacksRef = useRef({ onReady, onOpen, onClose, onSubmit, onError });
|
|
27
|
+
callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (typeof window === "undefined") return;
|
|
30
|
+
const instance = FeedValue.init({
|
|
31
|
+
widgetId,
|
|
32
|
+
apiBaseUrl,
|
|
33
|
+
config,
|
|
34
|
+
headless
|
|
35
|
+
});
|
|
36
|
+
instanceRef.current = instance;
|
|
37
|
+
const handleReady = () => callbacksRef.current.onReady?.();
|
|
38
|
+
const handleOpen = () => callbacksRef.current.onOpen?.();
|
|
39
|
+
const handleClose = () => callbacksRef.current.onClose?.();
|
|
40
|
+
const handleSubmit = (feedback) => callbacksRef.current.onSubmit?.(feedback);
|
|
41
|
+
const handleError = (error) => callbacksRef.current.onError?.(error);
|
|
42
|
+
instance.on("ready", handleReady);
|
|
43
|
+
instance.on("open", handleOpen);
|
|
44
|
+
instance.on("close", handleClose);
|
|
45
|
+
instance.on("submit", handleSubmit);
|
|
46
|
+
instance.on("error", handleError);
|
|
47
|
+
return () => {
|
|
48
|
+
instance.off("ready", handleReady);
|
|
49
|
+
instance.off("open", handleOpen);
|
|
50
|
+
instance.off("close", handleClose);
|
|
51
|
+
instance.off("submit", handleSubmit);
|
|
52
|
+
instance.off("error", handleError);
|
|
53
|
+
instance.destroy();
|
|
54
|
+
instanceRef.current = null;
|
|
55
|
+
};
|
|
56
|
+
}, [widgetId, apiBaseUrl, headless]);
|
|
57
|
+
const state = useSyncExternalStore(
|
|
58
|
+
// Subscribe function
|
|
59
|
+
(callback) => {
|
|
60
|
+
const instance = instanceRef.current;
|
|
61
|
+
if (!instance) return () => {
|
|
62
|
+
};
|
|
63
|
+
return instance.subscribe(callback);
|
|
64
|
+
},
|
|
65
|
+
// getSnapshot - client
|
|
66
|
+
() => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),
|
|
67
|
+
// getServerSnapshot - SSR
|
|
68
|
+
getServerSnapshot
|
|
69
|
+
);
|
|
70
|
+
const open = useCallback(() => instanceRef.current?.open(), []);
|
|
71
|
+
const close = useCallback(() => instanceRef.current?.close(), []);
|
|
72
|
+
const toggle = useCallback(() => instanceRef.current?.toggle(), []);
|
|
73
|
+
const show = useCallback(() => instanceRef.current?.show(), []);
|
|
74
|
+
const hide = useCallback(() => instanceRef.current?.hide(), []);
|
|
75
|
+
const submit = useCallback(
|
|
76
|
+
(feedback) => instanceRef.current?.submit(feedback) ?? Promise.reject(new Error("Not initialized")),
|
|
77
|
+
[]
|
|
78
|
+
);
|
|
79
|
+
const identify = useCallback(
|
|
80
|
+
(userId, traits) => instanceRef.current?.identify(userId, traits),
|
|
81
|
+
[]
|
|
82
|
+
);
|
|
83
|
+
const setData = useCallback(
|
|
84
|
+
(data) => instanceRef.current?.setData(data),
|
|
85
|
+
[]
|
|
86
|
+
);
|
|
87
|
+
const reset = useCallback(() => instanceRef.current?.reset(), []);
|
|
88
|
+
const value = useMemo(
|
|
89
|
+
() => ({
|
|
90
|
+
instance: instanceRef.current,
|
|
91
|
+
isReady: state.isReady,
|
|
92
|
+
isOpen: state.isOpen,
|
|
93
|
+
isVisible: state.isVisible,
|
|
94
|
+
error: state.error,
|
|
95
|
+
isSubmitting: state.isSubmitting,
|
|
96
|
+
isHeadless: headless ?? false,
|
|
97
|
+
open,
|
|
98
|
+
close,
|
|
99
|
+
toggle,
|
|
100
|
+
show,
|
|
101
|
+
hide,
|
|
102
|
+
submit,
|
|
103
|
+
identify,
|
|
104
|
+
setData,
|
|
105
|
+
reset
|
|
106
|
+
}),
|
|
107
|
+
[state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]
|
|
108
|
+
);
|
|
109
|
+
return /* @__PURE__ */ jsx(FeedValueContext.Provider, { value, children });
|
|
110
|
+
}
|
|
111
|
+
function useFeedValue() {
|
|
112
|
+
const context = useContext(FeedValueContext);
|
|
113
|
+
if (!context) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
'useFeedValue must be used within a FeedValueProvider. Make sure to wrap your app with <FeedValueProvider widgetId="...">.'
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return context;
|
|
119
|
+
}
|
|
120
|
+
function useFeedValueOptional() {
|
|
121
|
+
return useContext(FeedValueContext);
|
|
122
|
+
}
|
|
123
|
+
function FeedValueWidget(props) {
|
|
124
|
+
return /* @__PURE__ */ jsx(FeedValueProvider, { ...props, children: null });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export { FeedValueProvider, FeedValueWidget, useFeedValue, useFeedValueOptional };
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
129
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider.tsx","../src/components.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["'use client';\n\n/**\n * @feedvalue/react - Provider\n *\n * FeedValueProvider component for React applications.\n * Uses useSyncExternalStore for React 18+ concurrent mode compatibility.\n */\n\nimport React, {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useSyncExternalStore,\n useMemo,\n type ReactNode,\n} from 'react';\nimport {\n FeedValue,\n type FeedValueConfig,\n type FeedValueState,\n type FeedbackData,\n type UserTraits,\n} from '@feedvalue/core';\n\n/**\n * Context value provided by FeedValueProvider\n */\nexport interface FeedValueContextValue {\n /** FeedValue instance (for advanced usage) */\n instance: FeedValue | null;\n /** Widget is ready (config loaded) */\n isReady: boolean;\n /** Modal is open */\n isOpen: boolean;\n /** Widget is visible */\n isVisible: boolean;\n /** Current error (if any) */\n error: Error | null;\n /** Submission in progress */\n isSubmitting: boolean;\n /** Running in headless mode (no default UI rendered) */\n isHeadless: boolean;\n /** Open the feedback modal */\n open: () => void;\n /** Close the feedback modal */\n close: () => void;\n /** Toggle the feedback modal */\n toggle: () => void;\n /** Show the trigger button */\n show: () => void;\n /** Hide the trigger button */\n hide: () => void;\n /** Submit feedback programmatically */\n submit: (feedback: Partial<FeedbackData>) => Promise<void>;\n /** Identify user */\n identify: (userId: string, traits?: UserTraits) => void;\n /** Set user data */\n setData: (data: Record<string, string>) => void;\n /** Reset user data */\n reset: () => void;\n}\n\n/**\n * FeedValue context (internal)\n */\nconst FeedValueContext = createContext<FeedValueContextValue | null>(null);\n\n/**\n * Props for FeedValueProvider\n */\nexport interface FeedValueProviderProps {\n /** Widget ID from FeedValue dashboard */\n widgetId: string;\n /** API base URL (for self-hosted) */\n apiBaseUrl?: string;\n /** Configuration overrides */\n config?: Partial<FeedValueConfig>;\n /**\n * Headless mode - disables all DOM rendering.\n * Use this when you want full control over the UI.\n * The SDK will still fetch config and provide all API methods\n * but won't render any trigger button or modal.\n *\n * @default false\n */\n headless?: boolean;\n /** Children */\n children: ReactNode;\n /** Callback when widget is ready */\n onReady?: () => void;\n /** Callback when modal opens */\n onOpen?: () => void;\n /** Callback when modal closes */\n onClose?: () => void;\n /** Callback when feedback is submitted */\n onSubmit?: (feedback: FeedbackData) => void;\n /** Callback when an error occurs */\n onError?: (error: Error) => void;\n}\n\n/**\n * Server snapshot for SSR - always returns initial state\n * This prevents hydration mismatches\n */\nconst getServerSnapshot = (): FeedValueState => ({\n isReady: false,\n isOpen: false,\n isVisible: false,\n error: null,\n isSubmitting: false,\n});\n\n/**\n * FeedValueProvider component\n *\n * Provides FeedValue context to child components.\n *\n * @example\n * ```tsx\n * // app/layout.tsx (Next.js App Router)\n * import { FeedValueProvider } from '@feedvalue/react';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html>\n * <body>\n * <FeedValueProvider\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Submitted:', feedback)}\n * >\n * {children}\n * </FeedValueProvider>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\nexport function FeedValueProvider({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n children,\n onReady,\n onOpen,\n onClose,\n onSubmit,\n onError,\n}: FeedValueProviderProps): React.ReactElement {\n const instanceRef = useRef<FeedValue | null>(null);\n const callbacksRef = useRef({ onReady, onOpen, onClose, onSubmit, onError });\n\n // Keep callbacks ref updated\n callbacksRef.current = { onReady, onOpen, onClose, onSubmit, onError };\n\n // Initialize on mount\n useEffect(() => {\n // Only initialize on client side\n if (typeof window === 'undefined') return;\n\n const instance = FeedValue.init({\n widgetId,\n apiBaseUrl,\n config,\n headless,\n });\n instanceRef.current = instance;\n\n // Subscribe to events\n const handleReady = () => callbacksRef.current.onReady?.();\n const handleOpen = () => callbacksRef.current.onOpen?.();\n const handleClose = () => callbacksRef.current.onClose?.();\n const handleSubmit = (feedback: FeedbackData) => callbacksRef.current.onSubmit?.(feedback);\n const handleError = (error: Error) => callbacksRef.current.onError?.(error);\n\n instance.on('ready', handleReady);\n instance.on('open', handleOpen);\n instance.on('close', handleClose);\n instance.on('submit', handleSubmit);\n instance.on('error', handleError);\n\n return () => {\n instance.off('ready', handleReady);\n instance.off('open', handleOpen);\n instance.off('close', handleClose);\n instance.off('submit', handleSubmit);\n instance.off('error', handleError);\n instance.destroy();\n instanceRef.current = null;\n };\n }, [widgetId, apiBaseUrl, headless]); // Re-init if widgetId, apiBaseUrl, or headless changes\n\n // Use useSyncExternalStore for concurrent mode compatibility\n const state = useSyncExternalStore(\n // Subscribe function\n (callback) => {\n const instance = instanceRef.current;\n if (!instance) return () => {};\n return instance.subscribe(callback);\n },\n // getSnapshot - client\n () => instanceRef.current?.getSnapshot() ?? getServerSnapshot(),\n // getServerSnapshot - SSR\n getServerSnapshot\n );\n\n // Stable callback references to prevent recreation on every state change\n const open = useCallback(() => instanceRef.current?.open(), []);\n const close = useCallback(() => instanceRef.current?.close(), []);\n const toggle = useCallback(() => instanceRef.current?.toggle(), []);\n const show = useCallback(() => instanceRef.current?.show(), []);\n const hide = useCallback(() => instanceRef.current?.hide(), []);\n const submit = useCallback(\n (feedback: Partial<FeedbackData>) =>\n instanceRef.current?.submit(feedback) ?? Promise.reject(new Error('Not initialized')),\n []\n );\n const identify = useCallback(\n (userId: string, traits?: UserTraits) => instanceRef.current?.identify(userId, traits),\n []\n );\n const setData = useCallback(\n (data: Record<string, string>) => instanceRef.current?.setData(data),\n []\n );\n const reset = useCallback(() => instanceRef.current?.reset(), []);\n\n // Memoize context value to prevent unnecessary re-renders\n const value = useMemo<FeedValueContextValue>(\n () => ({\n instance: instanceRef.current,\n isReady: state.isReady,\n isOpen: state.isOpen,\n isVisible: state.isVisible,\n error: state.error,\n isSubmitting: state.isSubmitting,\n isHeadless: headless ?? false,\n open,\n close,\n toggle,\n show,\n hide,\n submit,\n identify,\n setData,\n reset,\n }),\n [state, headless, open, close, toggle, show, hide, submit, identify, setData, reset]\n );\n\n return (\n <FeedValueContext.Provider value={value}>\n {children}\n </FeedValueContext.Provider>\n );\n}\n\n/**\n * Hook to access FeedValue context\n *\n * Must be used within a FeedValueProvider.\n *\n * @example\n * ```tsx\n * 'use client';\n * import { useFeedValue } from '@feedvalue/react';\n *\n * export function FeedbackButton() {\n * const { open, isReady } = useFeedValue();\n *\n * return (\n * <button onClick={open} disabled={!isReady}>\n * Give Feedback\n * </button>\n * );\n * }\n * ```\n */\nexport function useFeedValue(): FeedValueContextValue {\n const context = useContext(FeedValueContext);\n\n if (!context) {\n throw new Error(\n 'useFeedValue must be used within a FeedValueProvider. ' +\n 'Make sure to wrap your app with <FeedValueProvider widgetId=\"...\">.'\n );\n }\n\n return context;\n}\n\n/**\n * Hook to check if inside FeedValueProvider\n * Returns null if outside provider, context value if inside\n */\nexport function useFeedValueOptional(): FeedValueContextValue | null {\n return useContext(FeedValueContext);\n}\n","'use client';\n\n/**\n * @feedvalue/react - Components\n *\n * Standalone React components for FeedValue.\n */\n\nimport React from 'react';\nimport { FeedValueProvider, type FeedValueProviderProps } from './provider';\n\n/**\n * Props for FeedValueWidget component\n */\nexport interface FeedValueWidgetProps extends Omit<FeedValueProviderProps, 'children'> {}\n\n/**\n * Standalone FeedValue widget component\n *\n * Use this when you don't need to access the FeedValue context elsewhere.\n * The widget renders itself via DOM injection - this component is a container.\n *\n * @example\n * ```tsx\n * // Simple usage - just drop in anywhere\n * import { FeedValueWidget } from '@feedvalue/react';\n *\n * export function App() {\n * return (\n * <div>\n * <h1>My App</h1>\n * <FeedValueWidget\n * widgetId=\"your-widget-id\"\n * onSubmit={(feedback) => console.log('Feedback:', feedback)}\n * />\n * </div>\n * );\n * }\n * ```\n */\nexport function FeedValueWidget(props: FeedValueWidgetProps): React.ReactElement {\n return (\n <FeedValueProvider {...props}>\n {/* Widget renders via DOM injection, no children needed */}\n {null}\n </FeedValueProvider>\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@feedvalue/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "FeedValue React SDK - Provider, Hooks, and Components for React 18+",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=18.0.0",
|
|
28
|
+
"react-dom": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@feedvalue/core": "^0.1.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
35
|
+
"@testing-library/react": "^16.1.0",
|
|
36
|
+
"@types/react": "^19.0.0",
|
|
37
|
+
"@types/react-dom": "^19.0.0",
|
|
38
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
39
|
+
"eslint": "^9.17.0",
|
|
40
|
+
"happy-dom": "^15.11.0",
|
|
41
|
+
"react": "^19.0.0",
|
|
42
|
+
"react-dom": "^19.0.0",
|
|
43
|
+
"tsup": "^8.3.0",
|
|
44
|
+
"typescript": "^5.7.0",
|
|
45
|
+
"vitest": "^2.1.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/sarverenterprises/feedvalue-packages.git",
|
|
53
|
+
"directory": "packages/react"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://feedvalue.com",
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/sarverenterprises/feedvalue-packages/issues"
|
|
58
|
+
},
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"author": "Sarver Enterprises",
|
|
61
|
+
"keywords": [
|
|
62
|
+
"feedvalue",
|
|
63
|
+
"feedback",
|
|
64
|
+
"widget",
|
|
65
|
+
"react",
|
|
66
|
+
"hooks",
|
|
67
|
+
"nextjs"
|
|
68
|
+
],
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": ">=18"
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"build": "tsup",
|
|
74
|
+
"dev": "tsup --watch",
|
|
75
|
+
"lint": "eslint src --ext .ts,.tsx",
|
|
76
|
+
"test": "vitest run",
|
|
77
|
+
"test:watch": "vitest",
|
|
78
|
+
"test:coverage": "vitest run --coverage",
|
|
79
|
+
"typecheck": "tsc --noEmit",
|
|
80
|
+
"clean": "rm -rf dist"
|
|
81
|
+
}
|
|
82
|
+
}
|