fdbck-react 0.2.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/LICENSE +21 -0
- package/README.md +145 -0
- package/dist/index.css +418 -0
- package/dist/index.d.mts +115 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.js +716 -0
- package/dist/index.mjs +688 -0
- package/package.json +52 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/components/FdbckWidget.tsx
|
|
6
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState5, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
// src/lib/errors.ts
|
|
9
|
+
var FdbckError = class extends Error {
|
|
10
|
+
constructor(code, message) {
|
|
11
|
+
super(message);
|
|
12
|
+
__publicField(this, "code");
|
|
13
|
+
this.name = "FdbckError";
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
function statusToErrorCode(status) {
|
|
18
|
+
switch (status) {
|
|
19
|
+
case "invalid":
|
|
20
|
+
case "token_expired":
|
|
21
|
+
return "invalid_token";
|
|
22
|
+
case "already_responded":
|
|
23
|
+
return "already_responded";
|
|
24
|
+
case "question_ended":
|
|
25
|
+
return "question_expired";
|
|
26
|
+
default:
|
|
27
|
+
return "unknown";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function httpStatusToErrorCode(status) {
|
|
31
|
+
if (status === 401) return "invalid_token";
|
|
32
|
+
if (status === 409) return "already_responded";
|
|
33
|
+
if (status === 410) return "question_expired";
|
|
34
|
+
return "unknown";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/lib/api.ts
|
|
38
|
+
var DEFAULT_BASE_URL = "https://api.fdbck.sh";
|
|
39
|
+
var FdbckApiClient = class {
|
|
40
|
+
constructor(baseUrl) {
|
|
41
|
+
__publicField(this, "baseUrl");
|
|
42
|
+
this.baseUrl = (baseUrl || DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
43
|
+
}
|
|
44
|
+
/** Fetch question data for a token via GET /v1/f/:token */
|
|
45
|
+
async fetchQuestion(token, signal) {
|
|
46
|
+
let res;
|
|
47
|
+
try {
|
|
48
|
+
res = await fetch(`${this.baseUrl}/v1/f/${encodeURIComponent(token)}`, { signal });
|
|
49
|
+
} catch (err) {
|
|
50
|
+
if (err.name === "AbortError") throw err;
|
|
51
|
+
throw new FdbckError("network_error", "Failed to fetch question");
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw new FdbckError(httpStatusToErrorCode(res.status), `HTTP ${res.status}`);
|
|
55
|
+
}
|
|
56
|
+
const data = await res.json();
|
|
57
|
+
if (data.status !== "valid") {
|
|
58
|
+
throw new FdbckError(statusToErrorCode(data.status), `Token status: ${data.status}`);
|
|
59
|
+
}
|
|
60
|
+
return data;
|
|
61
|
+
}
|
|
62
|
+
/** Submit a response via POST /v1/responses */
|
|
63
|
+
async submitResponse(token, value, signal) {
|
|
64
|
+
let res;
|
|
65
|
+
try {
|
|
66
|
+
res = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify({ token, value }),
|
|
70
|
+
signal
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (err.name === "AbortError") throw err;
|
|
74
|
+
throw new FdbckError("network_error", "Failed to submit response");
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const body = await res.json().catch(() => null);
|
|
78
|
+
const message = body?.error?.message || `HTTP ${res.status}`;
|
|
79
|
+
throw new FdbckError(httpStatusToErrorCode(res.status), message);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// src/lib/colors.ts
|
|
85
|
+
function getThemeVars(themeColor, themeMode) {
|
|
86
|
+
const accent = themeColor || "#FF6B35";
|
|
87
|
+
const isDark = themeMode === "dark";
|
|
88
|
+
return {
|
|
89
|
+
"--fdbck-accent": accent,
|
|
90
|
+
"--fdbck-accent-alpha": `${accent}18`,
|
|
91
|
+
"--fdbck-bg": isDark ? "#0a0a0a" : "#f9fafb",
|
|
92
|
+
"--fdbck-card-bg": isDark ? "#141414" : "#ffffff",
|
|
93
|
+
"--fdbck-border": isDark ? "#262626" : "#e5e7eb",
|
|
94
|
+
"--fdbck-text-primary": isDark ? "#ffffff" : "#111827",
|
|
95
|
+
"--fdbck-text-secondary": isDark ? "#9ca3af" : "#6b7280",
|
|
96
|
+
"--fdbck-text-muted": isDark ? "#6b7280" : "#9ca3af",
|
|
97
|
+
"--fdbck-indicator-border": isDark ? "#52525b" : "#d1d5db"
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/lib/style.ts
|
|
102
|
+
function getStyleVars(style) {
|
|
103
|
+
return {
|
|
104
|
+
"--fdbck-width": style?.width || "100%",
|
|
105
|
+
"--fdbck-max-width": style?.maxWidth || "28rem",
|
|
106
|
+
"--fdbck-border-radius": style?.borderRadius || "0.75rem",
|
|
107
|
+
"--fdbck-font-family": style?.fontFamily || "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
108
|
+
"--fdbck-font-size": style?.fontSize || "0.875rem"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// src/lib/locale.ts
|
|
113
|
+
var DEFAULT_LOCALE = {
|
|
114
|
+
submit: "Submit",
|
|
115
|
+
poweredBy: "Powered by",
|
|
116
|
+
loading: "Loading\u2026",
|
|
117
|
+
errorTitle: "Something went wrong",
|
|
118
|
+
errorMessage: "Unable to load this question. Please try again later.",
|
|
119
|
+
retry: "Retry",
|
|
120
|
+
close: "Close"
|
|
121
|
+
};
|
|
122
|
+
function resolveLocale(locale) {
|
|
123
|
+
if (!locale) return DEFAULT_LOCALE;
|
|
124
|
+
return { ...DEFAULT_LOCALE, ...locale };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/hooks/useWidgetState.ts
|
|
128
|
+
import { useReducer } from "react";
|
|
129
|
+
function reducer(state, action) {
|
|
130
|
+
switch (action.type) {
|
|
131
|
+
case "TOKEN_RESOLVED":
|
|
132
|
+
return { ...state, status: "loading", token: action.token };
|
|
133
|
+
case "LOADED":
|
|
134
|
+
return { ...state, status: "ready", question: action.question, token: action.token };
|
|
135
|
+
case "SUBMITTING":
|
|
136
|
+
return { ...state, status: "submitting" };
|
|
137
|
+
case "COMPLETE":
|
|
138
|
+
return { ...state, status: "complete", submittedValue: action.value };
|
|
139
|
+
case "ERROR":
|
|
140
|
+
return { ...state, status: "error", error: action.error };
|
|
141
|
+
case "RESET":
|
|
142
|
+
return initialState;
|
|
143
|
+
default:
|
|
144
|
+
return state;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
var initialState = {
|
|
148
|
+
status: "loading",
|
|
149
|
+
question: null,
|
|
150
|
+
token: null,
|
|
151
|
+
error: null,
|
|
152
|
+
submittedValue: null
|
|
153
|
+
};
|
|
154
|
+
var fetchingTokenState = {
|
|
155
|
+
...initialState,
|
|
156
|
+
status: "fetching_token"
|
|
157
|
+
};
|
|
158
|
+
function useWidgetState(needsTokenFetch) {
|
|
159
|
+
return useReducer(reducer, needsTokenFetch ? fetchingTokenState : initialState);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/hooks/useFocusTrap.ts
|
|
163
|
+
import { useEffect } from "react";
|
|
164
|
+
var FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
165
|
+
function useFocusTrap(containerRef, active) {
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (!active) return;
|
|
168
|
+
const container = containerRef.current;
|
|
169
|
+
if (!container) return;
|
|
170
|
+
function handleKeyDown(e) {
|
|
171
|
+
if (e.key !== "Tab") return;
|
|
172
|
+
const focusable = Array.from(container.querySelectorAll(FOCUSABLE));
|
|
173
|
+
if (focusable.length === 0) {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const first = focusable[0];
|
|
178
|
+
const last = focusable[focusable.length - 1];
|
|
179
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
last.focus();
|
|
182
|
+
} else if (!e.shiftKey && document.activeElement === last) {
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
first.focus();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
container.addEventListener("keydown", handleKeyDown);
|
|
188
|
+
const firstFocusable = container.querySelector(FOCUSABLE);
|
|
189
|
+
firstFocusable?.focus();
|
|
190
|
+
return () => container.removeEventListener("keydown", handleKeyDown);
|
|
191
|
+
}, [containerRef, active]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/components/FdbckShadowHost.tsx
|
|
195
|
+
import { useRef, useEffect as useEffect2, useState } from "react";
|
|
196
|
+
import { createPortal } from "react-dom";
|
|
197
|
+
|
|
198
|
+
// src/styles/base.css
|
|
199
|
+
var base_default = {};
|
|
200
|
+
|
|
201
|
+
// src/styles/question-types.css
|
|
202
|
+
var question_types_default = {};
|
|
203
|
+
|
|
204
|
+
// src/styles/confirmation.css
|
|
205
|
+
var confirmation_default = {};
|
|
206
|
+
|
|
207
|
+
// src/styles/branding.css
|
|
208
|
+
var branding_default = {};
|
|
209
|
+
|
|
210
|
+
// src/styles/modal.css
|
|
211
|
+
var modal_default = {};
|
|
212
|
+
|
|
213
|
+
// src/styles/popover.css
|
|
214
|
+
var popover_default = {};
|
|
215
|
+
|
|
216
|
+
// src/styles/index.ts
|
|
217
|
+
var WIDGET_CSS = [
|
|
218
|
+
base_default,
|
|
219
|
+
question_types_default,
|
|
220
|
+
confirmation_default,
|
|
221
|
+
branding_default,
|
|
222
|
+
modal_default,
|
|
223
|
+
popover_default
|
|
224
|
+
].join("\n");
|
|
225
|
+
|
|
226
|
+
// src/components/FdbckShadowHost.tsx
|
|
227
|
+
import { jsx } from "react/jsx-runtime";
|
|
228
|
+
function FdbckShadowHost({ children, style }) {
|
|
229
|
+
const hostRef = useRef(null);
|
|
230
|
+
const [container, setContainer] = useState(null);
|
|
231
|
+
useEffect2(() => {
|
|
232
|
+
const host = hostRef.current;
|
|
233
|
+
if (!host || host.shadowRoot) return;
|
|
234
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
235
|
+
const styleEl = document.createElement("style");
|
|
236
|
+
styleEl.textContent = WIDGET_CSS;
|
|
237
|
+
shadow.appendChild(styleEl);
|
|
238
|
+
const wrapper = document.createElement("div");
|
|
239
|
+
shadow.appendChild(wrapper);
|
|
240
|
+
setContainer(wrapper);
|
|
241
|
+
}, []);
|
|
242
|
+
useEffect2(() => {
|
|
243
|
+
if (!container || !style) return;
|
|
244
|
+
const parent = container.parentNode;
|
|
245
|
+
const host = parent?.host;
|
|
246
|
+
if (host) {
|
|
247
|
+
for (const [key, value] of Object.entries(style)) {
|
|
248
|
+
host.style.setProperty(key, value);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}, [container, style]);
|
|
252
|
+
return /* @__PURE__ */ jsx("div", { ref: hostRef, children: container && createPortal(children, container) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/components/FdbckLoading.tsx
|
|
256
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
257
|
+
function FdbckLoading() {
|
|
258
|
+
return /* @__PURE__ */ jsx2("div", { className: "fdbck-loading", children: /* @__PURE__ */ jsx2("div", { className: "fdbck-spinner" }) });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/components/FdbckConfirmation.tsx
|
|
262
|
+
import { jsx as jsx3, jsxs } from "react/jsx-runtime";
|
|
263
|
+
function FdbckConfirmation({ thankYouMessage }) {
|
|
264
|
+
return /* @__PURE__ */ jsxs("div", { className: "fdbck-confirmation", children: [
|
|
265
|
+
/* @__PURE__ */ jsx3("div", { className: "fdbck-check-circle", children: /* @__PURE__ */ jsx3("svg", { fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, children: /* @__PURE__ */ jsx3("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) }),
|
|
266
|
+
/* @__PURE__ */ jsx3("h2", { children: thankYouMessage ? "Thank you" : "Response recorded" }),
|
|
267
|
+
/* @__PURE__ */ jsx3("p", { children: thankYouMessage || "Your response has been recorded. You can close this." })
|
|
268
|
+
] });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/components/FdbckError.tsx
|
|
272
|
+
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
273
|
+
var ERROR_TITLES = {
|
|
274
|
+
invalid_token: "Invalid link",
|
|
275
|
+
already_responded: "Already responded",
|
|
276
|
+
question_expired: "Question closed"
|
|
277
|
+
};
|
|
278
|
+
var ERROR_MESSAGES = {
|
|
279
|
+
invalid_token: "This response link is not valid.",
|
|
280
|
+
already_responded: "You've already responded to this.",
|
|
281
|
+
question_expired: "This question has closed."
|
|
282
|
+
};
|
|
283
|
+
function FdbckErrorDisplay({ error, locale, onRetry }) {
|
|
284
|
+
const title = ERROR_TITLES[error.code] || locale.errorTitle;
|
|
285
|
+
const message = ERROR_MESSAGES[error.code] || locale.errorMessage;
|
|
286
|
+
const isRetryable = error.code === "network_error" || error.code === "unknown";
|
|
287
|
+
return /* @__PURE__ */ jsxs2("div", { className: "fdbck-error-state", children: [
|
|
288
|
+
/* @__PURE__ */ jsx4("h2", { children: title }),
|
|
289
|
+
/* @__PURE__ */ jsx4("p", { children: message }),
|
|
290
|
+
isRetryable && onRetry && /* @__PURE__ */ jsx4("button", { className: "fdbck-retry-btn", onClick: onRetry, children: locale.retry })
|
|
291
|
+
] });
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/components/FdbckHeader.tsx
|
|
295
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
296
|
+
function FdbckHeader({ question }) {
|
|
297
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
298
|
+
question.welcome_message && /* @__PURE__ */ jsx5("p", { className: "fdbck-welcome", children: question.welcome_message }),
|
|
299
|
+
/* @__PURE__ */ jsx5("h1", { className: "fdbck-question", children: question.question })
|
|
300
|
+
] });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/components/questions/YesNoQuestion.tsx
|
|
304
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
305
|
+
function YesNoQuestion({ options, disabled, onSubmit }) {
|
|
306
|
+
return /* @__PURE__ */ jsx6("div", { className: "fdbck-yesno-grid", children: options.map((opt) => /* @__PURE__ */ jsx6(
|
|
307
|
+
"button",
|
|
308
|
+
{
|
|
309
|
+
className: "fdbck-option-btn",
|
|
310
|
+
disabled,
|
|
311
|
+
onClick: () => onSubmit(opt),
|
|
312
|
+
children: opt
|
|
313
|
+
},
|
|
314
|
+
opt
|
|
315
|
+
)) });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/components/questions/SingleChoiceQuestion.tsx
|
|
319
|
+
import { useState as useState2 } from "react";
|
|
320
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
321
|
+
function SingleChoiceQuestion({ options, disabled, onSubmit }) {
|
|
322
|
+
const [selected, setSelected] = useState2(null);
|
|
323
|
+
function handleClick(opt) {
|
|
324
|
+
setSelected(opt);
|
|
325
|
+
onSubmit(opt);
|
|
326
|
+
}
|
|
327
|
+
return /* @__PURE__ */ jsx7("div", { className: "fdbck-choice-list", children: options.map((opt) => {
|
|
328
|
+
const isSelected = selected === opt;
|
|
329
|
+
return /* @__PURE__ */ jsxs4(
|
|
330
|
+
"button",
|
|
331
|
+
{
|
|
332
|
+
className: "fdbck-option-btn",
|
|
333
|
+
"data-selected": isSelected,
|
|
334
|
+
disabled,
|
|
335
|
+
onClick: () => handleClick(opt),
|
|
336
|
+
children: [
|
|
337
|
+
/* @__PURE__ */ jsx7("span", { className: "fdbck-radio", "data-selected": isSelected, children: isSelected && /* @__PURE__ */ jsx7("span", { className: "fdbck-radio-dot" }) }),
|
|
338
|
+
opt
|
|
339
|
+
]
|
|
340
|
+
},
|
|
341
|
+
opt
|
|
342
|
+
);
|
|
343
|
+
}) });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// src/components/questions/MultipleChoiceQuestion.tsx
|
|
347
|
+
import { useState as useState3 } from "react";
|
|
348
|
+
import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
349
|
+
function MultipleChoiceQuestion({ options, disabled, locale, onSubmit }) {
|
|
350
|
+
const [selected, setSelected] = useState3([]);
|
|
351
|
+
function toggle(opt) {
|
|
352
|
+
setSelected(
|
|
353
|
+
(prev) => prev.includes(opt) ? prev.filter((x) => x !== opt) : [...prev, opt]
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fdbck-choice-list", children: [
|
|
357
|
+
options.map((opt) => {
|
|
358
|
+
const isSelected = selected.includes(opt);
|
|
359
|
+
return /* @__PURE__ */ jsxs5(
|
|
360
|
+
"button",
|
|
361
|
+
{
|
|
362
|
+
className: "fdbck-option-btn",
|
|
363
|
+
"data-selected": isSelected,
|
|
364
|
+
disabled,
|
|
365
|
+
onClick: () => toggle(opt),
|
|
366
|
+
children: [
|
|
367
|
+
/* @__PURE__ */ jsx8("span", { className: "fdbck-checkbox", "data-selected": isSelected, children: isSelected && /* @__PURE__ */ jsx8("svg", { fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 3, children: /* @__PURE__ */ jsx8("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M5 13l4 4L19 7" }) }) }),
|
|
368
|
+
opt
|
|
369
|
+
]
|
|
370
|
+
},
|
|
371
|
+
opt
|
|
372
|
+
);
|
|
373
|
+
}),
|
|
374
|
+
/* @__PURE__ */ jsx8(
|
|
375
|
+
"button",
|
|
376
|
+
{
|
|
377
|
+
className: "fdbck-submit-btn",
|
|
378
|
+
disabled: disabled || selected.length === 0,
|
|
379
|
+
onClick: () => onSubmit(selected),
|
|
380
|
+
children: locale.submit
|
|
381
|
+
}
|
|
382
|
+
)
|
|
383
|
+
] });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/components/questions/RatingQuestion.tsx
|
|
387
|
+
import { useState as useState4 } from "react";
|
|
388
|
+
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
389
|
+
function RatingQuestion({ options, disabled, onSubmit }) {
|
|
390
|
+
const [selected, setSelected] = useState4(null);
|
|
391
|
+
const count = options.max - options.min + 1;
|
|
392
|
+
const columns = Math.min(count, 10);
|
|
393
|
+
function handleClick(v) {
|
|
394
|
+
setSelected(v);
|
|
395
|
+
onSubmit(v);
|
|
396
|
+
}
|
|
397
|
+
return /* @__PURE__ */ jsxs6("div", { children: [
|
|
398
|
+
/* @__PURE__ */ jsx9("div", { className: "fdbck-rating-grid", style: { gridTemplateColumns: `repeat(${columns}, 1fr)` }, children: Array.from({ length: count }, (_, i) => options.min + i).map((v) => /* @__PURE__ */ jsx9(
|
|
399
|
+
"button",
|
|
400
|
+
{
|
|
401
|
+
className: "fdbck-rating-btn",
|
|
402
|
+
"data-selected": selected === v,
|
|
403
|
+
disabled,
|
|
404
|
+
onClick: () => handleClick(v),
|
|
405
|
+
children: v
|
|
406
|
+
},
|
|
407
|
+
v
|
|
408
|
+
)) }),
|
|
409
|
+
/* @__PURE__ */ jsxs6("div", { className: "fdbck-rating-labels", children: [
|
|
410
|
+
/* @__PURE__ */ jsx9("span", { children: options.min_label || options.min }),
|
|
411
|
+
/* @__PURE__ */ jsx9("span", { children: options.max_label || options.max })
|
|
412
|
+
] })
|
|
413
|
+
] });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/components/FdbckBody.tsx
|
|
417
|
+
import { jsx as jsx10 } from "react/jsx-runtime";
|
|
418
|
+
function FdbckBody({ question, disabled, locale, onSubmit }) {
|
|
419
|
+
switch (question.type) {
|
|
420
|
+
case "yes_no":
|
|
421
|
+
return /* @__PURE__ */ jsx10(YesNoQuestion, { options: question.options, disabled, onSubmit });
|
|
422
|
+
case "single_choice":
|
|
423
|
+
return /* @__PURE__ */ jsx10(SingleChoiceQuestion, { options: question.options, disabled, onSubmit });
|
|
424
|
+
case "multiple_choice":
|
|
425
|
+
return /* @__PURE__ */ jsx10(MultipleChoiceQuestion, { options: question.options, disabled, locale, onSubmit });
|
|
426
|
+
case "rating":
|
|
427
|
+
return /* @__PURE__ */ jsx10(RatingQuestion, { options: question.options, disabled, onSubmit });
|
|
428
|
+
default:
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/components/FdbckFooter.tsx
|
|
434
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
435
|
+
function FdbckFooter({ hideBranding, locale }) {
|
|
436
|
+
if (hideBranding) return null;
|
|
437
|
+
return /* @__PURE__ */ jsx11("div", { className: "fdbck-branding", children: /* @__PURE__ */ jsxs7("a", { href: "https://fdbck.sh", target: "_blank", rel: "noopener noreferrer", children: [
|
|
438
|
+
locale.poweredBy,
|
|
439
|
+
" ",
|
|
440
|
+
/* @__PURE__ */ jsx11("span", { className: "fdbck-branding-name", children: "fdbck" })
|
|
441
|
+
] }) });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// src/components/FdbckCard.tsx
|
|
445
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
446
|
+
function FdbckCard({ state, locale, submitError, onSubmit, onRetry }) {
|
|
447
|
+
const { status, question, error } = state;
|
|
448
|
+
if (status === "fetching_token" || status === "loading") {
|
|
449
|
+
return /* @__PURE__ */ jsx12("div", { className: "fdbck-card", children: /* @__PURE__ */ jsx12(FdbckLoading, {}) });
|
|
450
|
+
}
|
|
451
|
+
if (status === "error" && error) {
|
|
452
|
+
const showBranding = error.code !== "invalid_token" && error.code !== "unknown" && !!question;
|
|
453
|
+
return /* @__PURE__ */ jsxs8("div", { className: "fdbck-card", children: [
|
|
454
|
+
/* @__PURE__ */ jsx12(FdbckErrorDisplay, { error, locale, onRetry }),
|
|
455
|
+
showBranding && /* @__PURE__ */ jsx12(FdbckFooter, { hideBranding: question?.hide_branding ?? true, locale })
|
|
456
|
+
] });
|
|
457
|
+
}
|
|
458
|
+
if (status === "complete" && question) {
|
|
459
|
+
return /* @__PURE__ */ jsxs8("div", { className: "fdbck-card", children: [
|
|
460
|
+
/* @__PURE__ */ jsx12(FdbckConfirmation, { thankYouMessage: question.thank_you_message }),
|
|
461
|
+
/* @__PURE__ */ jsx12(FdbckFooter, { hideBranding: question.hide_branding, locale })
|
|
462
|
+
] });
|
|
463
|
+
}
|
|
464
|
+
if ((status === "ready" || status === "submitting") && question) {
|
|
465
|
+
return /* @__PURE__ */ jsxs8("div", { className: "fdbck-card", children: [
|
|
466
|
+
/* @__PURE__ */ jsx12(FdbckHeader, { question }),
|
|
467
|
+
submitError && /* @__PURE__ */ jsx12("p", { className: "fdbck-error-text", children: submitError }),
|
|
468
|
+
/* @__PURE__ */ jsx12(
|
|
469
|
+
FdbckBody,
|
|
470
|
+
{
|
|
471
|
+
question,
|
|
472
|
+
disabled: status === "submitting",
|
|
473
|
+
locale,
|
|
474
|
+
onSubmit
|
|
475
|
+
}
|
|
476
|
+
),
|
|
477
|
+
/* @__PURE__ */ jsx12(FdbckFooter, { hideBranding: question.hide_branding, locale })
|
|
478
|
+
] });
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/components/FdbckWidget.tsx
|
|
484
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
485
|
+
function FdbckWidget({
|
|
486
|
+
token,
|
|
487
|
+
mode = "inline",
|
|
488
|
+
open = true,
|
|
489
|
+
baseUrl,
|
|
490
|
+
delay = 0,
|
|
491
|
+
autoCloseAfter,
|
|
492
|
+
closeOnOverlayClick = true,
|
|
493
|
+
closeOnEscape = true,
|
|
494
|
+
locale: localeProp,
|
|
495
|
+
style: styleProp,
|
|
496
|
+
onSubmit,
|
|
497
|
+
onDismiss,
|
|
498
|
+
onError,
|
|
499
|
+
onLoad
|
|
500
|
+
}) {
|
|
501
|
+
const [state, dispatch] = useWidgetState(false);
|
|
502
|
+
const [submitError, setSubmitError] = useState5(null);
|
|
503
|
+
const [visible, setVisible] = useState5(delay === 0);
|
|
504
|
+
const clientRef = useRef2(new FdbckApiClient(baseUrl));
|
|
505
|
+
const submitRef = useRef2(false);
|
|
506
|
+
const modalRef = useRef2(null);
|
|
507
|
+
const locale = resolveLocale(localeProp);
|
|
508
|
+
useEffect3(() => {
|
|
509
|
+
if (delay <= 0 || visible) return;
|
|
510
|
+
const timer = setTimeout(() => setVisible(true), delay);
|
|
511
|
+
return () => clearTimeout(timer);
|
|
512
|
+
}, [delay, visible]);
|
|
513
|
+
const loadQuestion = useCallback(() => {
|
|
514
|
+
const controller = new AbortController();
|
|
515
|
+
clientRef.current.fetchQuestion(token, controller.signal).then((data) => {
|
|
516
|
+
dispatch({ type: "LOADED", question: data.question, token: data.token });
|
|
517
|
+
onLoad?.(data.question);
|
|
518
|
+
}).catch((err) => {
|
|
519
|
+
if (err.name === "AbortError") return;
|
|
520
|
+
const fdbckErr = err instanceof FdbckError ? err : new FdbckError("unknown", err.message);
|
|
521
|
+
dispatch({ type: "ERROR", error: fdbckErr });
|
|
522
|
+
onError?.(fdbckErr);
|
|
523
|
+
});
|
|
524
|
+
return () => controller.abort();
|
|
525
|
+
}, [token, onLoad, onError]);
|
|
526
|
+
useEffect3(() => {
|
|
527
|
+
return loadQuestion();
|
|
528
|
+
}, [loadQuestion]);
|
|
529
|
+
function handleSubmit(value) {
|
|
530
|
+
if (submitRef.current) return;
|
|
531
|
+
submitRef.current = true;
|
|
532
|
+
setSubmitError(null);
|
|
533
|
+
dispatch({ type: "SUBMITTING" });
|
|
534
|
+
clientRef.current.submitResponse(state.token || token, value).then(() => {
|
|
535
|
+
dispatch({ type: "COMPLETE", value });
|
|
536
|
+
onSubmit?.(value);
|
|
537
|
+
if (autoCloseAfter && autoCloseAfter > 0) {
|
|
538
|
+
setTimeout(() => onDismiss?.(), autoCloseAfter);
|
|
539
|
+
}
|
|
540
|
+
}).catch((err) => {
|
|
541
|
+
const msg = err instanceof FdbckError ? err.message : "Something went wrong";
|
|
542
|
+
setSubmitError(msg);
|
|
543
|
+
dispatch({ type: "LOADED", question: state.question, token: state.token });
|
|
544
|
+
submitRef.current = false;
|
|
545
|
+
onError?.(err instanceof FdbckError ? err : new FdbckError("unknown", String(err)));
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
function handleRetry() {
|
|
549
|
+
dispatch({ type: "RESET" });
|
|
550
|
+
submitRef.current = false;
|
|
551
|
+
setSubmitError(null);
|
|
552
|
+
loadQuestion();
|
|
553
|
+
}
|
|
554
|
+
useEffect3(() => {
|
|
555
|
+
if (mode === "inline" || !closeOnEscape || !open) return;
|
|
556
|
+
function handleKeyDown(e) {
|
|
557
|
+
if (e.key === "Escape") onDismiss?.();
|
|
558
|
+
}
|
|
559
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
560
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
561
|
+
}, [mode, closeOnEscape, open, onDismiss]);
|
|
562
|
+
useFocusTrap(modalRef, mode === "modal" && open && visible);
|
|
563
|
+
if (!open || !visible) return null;
|
|
564
|
+
const themeVars = getThemeVars(state.question?.theme_color ?? null, state.question?.theme_mode ?? "dark");
|
|
565
|
+
const styleVars = getStyleVars(styleProp);
|
|
566
|
+
const cssVars = { ...themeVars, ...styleVars };
|
|
567
|
+
const card = /* @__PURE__ */ jsx13(
|
|
568
|
+
FdbckCard,
|
|
569
|
+
{
|
|
570
|
+
state,
|
|
571
|
+
locale,
|
|
572
|
+
submitError,
|
|
573
|
+
onSubmit: handleSubmit,
|
|
574
|
+
onRetry: handleRetry
|
|
575
|
+
}
|
|
576
|
+
);
|
|
577
|
+
if (mode === "modal") {
|
|
578
|
+
return /* @__PURE__ */ jsx13(FdbckShadowHost, { style: cssVars, children: /* @__PURE__ */ jsx13(
|
|
579
|
+
"div",
|
|
580
|
+
{
|
|
581
|
+
className: "fdbck-modal-backdrop",
|
|
582
|
+
onClick: closeOnOverlayClick ? onDismiss : void 0,
|
|
583
|
+
role: "dialog",
|
|
584
|
+
"aria-modal": "true",
|
|
585
|
+
children: /* @__PURE__ */ jsxs9(
|
|
586
|
+
"div",
|
|
587
|
+
{
|
|
588
|
+
ref: modalRef,
|
|
589
|
+
className: "fdbck-modal-content",
|
|
590
|
+
onClick: (e) => e.stopPropagation(),
|
|
591
|
+
children: [
|
|
592
|
+
/* @__PURE__ */ jsx13("button", { className: "fdbck-close-btn", onClick: onDismiss, "aria-label": locale.close, children: "\xD7" }),
|
|
593
|
+
card
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
) });
|
|
599
|
+
}
|
|
600
|
+
if (mode === "popover") {
|
|
601
|
+
return /* @__PURE__ */ jsx13(FdbckShadowHost, { style: cssVars, children: /* @__PURE__ */ jsxs9("div", { className: "fdbck-popover", role: "dialog", children: [
|
|
602
|
+
/* @__PURE__ */ jsx13("button", { className: "fdbck-close-btn", onClick: onDismiss, "aria-label": locale.close, children: "\xD7" }),
|
|
603
|
+
card
|
|
604
|
+
] }) });
|
|
605
|
+
}
|
|
606
|
+
return /* @__PURE__ */ jsx13(FdbckShadowHost, { style: cssVars, children: card });
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/components/FdbckProvider.tsx
|
|
610
|
+
import { createContext, useState as useState6, useCallback as useCallback2, useRef as useRef3 } from "react";
|
|
611
|
+
import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
612
|
+
var FdbckContext = createContext(null);
|
|
613
|
+
function FdbckProvider({ baseUrl, locale, style, children }) {
|
|
614
|
+
const [activeConfig, setActiveConfig] = useState6(null);
|
|
615
|
+
const resolverRef = useRef3(null);
|
|
616
|
+
const show = useCallback2((options) => {
|
|
617
|
+
return new Promise((resolve, reject) => {
|
|
618
|
+
resolverRef.current = { resolve, reject };
|
|
619
|
+
setActiveConfig(options);
|
|
620
|
+
});
|
|
621
|
+
}, []);
|
|
622
|
+
const dismiss = useCallback2(() => {
|
|
623
|
+
if (resolverRef.current) {
|
|
624
|
+
resolverRef.current.resolve({ status: "dismissed" });
|
|
625
|
+
resolverRef.current = null;
|
|
626
|
+
}
|
|
627
|
+
setActiveConfig(null);
|
|
628
|
+
}, []);
|
|
629
|
+
const handleSubmit = useCallback2((value) => {
|
|
630
|
+
if (resolverRef.current) {
|
|
631
|
+
resolverRef.current.resolve({ status: "submitted", value });
|
|
632
|
+
resolverRef.current = null;
|
|
633
|
+
}
|
|
634
|
+
setActiveConfig(null);
|
|
635
|
+
}, []);
|
|
636
|
+
const handleError = useCallback2((err) => {
|
|
637
|
+
activeConfig;
|
|
638
|
+
}, []);
|
|
639
|
+
const contextValue = {
|
|
640
|
+
show,
|
|
641
|
+
dismiss,
|
|
642
|
+
isActive: activeConfig !== null,
|
|
643
|
+
baseUrl,
|
|
644
|
+
locale,
|
|
645
|
+
style
|
|
646
|
+
};
|
|
647
|
+
return /* @__PURE__ */ jsxs10(FdbckContext.Provider, { value: contextValue, children: [
|
|
648
|
+
children,
|
|
649
|
+
activeConfig && /* @__PURE__ */ jsx14(
|
|
650
|
+
FdbckWidget,
|
|
651
|
+
{
|
|
652
|
+
token: activeConfig.token,
|
|
653
|
+
mode: activeConfig.mode || "modal",
|
|
654
|
+
open: activeConfig.open ?? true,
|
|
655
|
+
baseUrl: activeConfig.baseUrl || baseUrl,
|
|
656
|
+
delay: activeConfig.delay,
|
|
657
|
+
autoCloseAfter: activeConfig.autoCloseAfter,
|
|
658
|
+
closeOnOverlayClick: activeConfig.closeOnOverlayClick,
|
|
659
|
+
closeOnEscape: activeConfig.closeOnEscape,
|
|
660
|
+
locale: activeConfig.locale || locale,
|
|
661
|
+
style: activeConfig.style || style,
|
|
662
|
+
onSubmit: handleSubmit,
|
|
663
|
+
onDismiss: dismiss,
|
|
664
|
+
onError: handleError
|
|
665
|
+
}
|
|
666
|
+
)
|
|
667
|
+
] });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/hooks/useFdbck.ts
|
|
671
|
+
import { useContext } from "react";
|
|
672
|
+
function useFdbck() {
|
|
673
|
+
const context = useContext(FdbckContext);
|
|
674
|
+
if (!context) {
|
|
675
|
+
throw new Error("useFdbck must be used within a <FdbckProvider>");
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
show: context.show,
|
|
679
|
+
dismiss: context.dismiss,
|
|
680
|
+
isActive: context.isActive
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
export {
|
|
684
|
+
FdbckError,
|
|
685
|
+
FdbckProvider,
|
|
686
|
+
FdbckWidget,
|
|
687
|
+
useFdbck
|
|
688
|
+
};
|