@shellapps/experience-react 1.2.0 → 1.3.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/dist/index.d.mts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +531 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +529 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -35,6 +35,22 @@ declare class ErrorBoundary extends React__default.Component<ErrorBoundaryProps,
|
|
|
35
35
|
render(): string | number | boolean | Iterable<React__default.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
interface FeedbackButtonProps {
|
|
39
|
+
/** Position of the floating button */
|
|
40
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
41
|
+
/** Custom button label */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Button colour */
|
|
44
|
+
color?: string;
|
|
45
|
+
/** Hide the floating button (use exp.openFeedback() instead) */
|
|
46
|
+
hidden?: boolean;
|
|
47
|
+
/** API endpoint base URL */
|
|
48
|
+
apiUrl?: string;
|
|
49
|
+
/** Auth token for user endpoints */
|
|
50
|
+
authToken?: string;
|
|
51
|
+
}
|
|
52
|
+
declare function FeedbackButton({ position, label, color, hidden, apiUrl, authToken, }: FeedbackButtonProps): react_jsx_runtime.JSX.Element;
|
|
53
|
+
|
|
38
54
|
declare function useExperience(): _shellapps_experience.Experience;
|
|
39
55
|
declare function useTrack(): {
|
|
40
56
|
track: (eventName: string, metadata?: Record<string, string>) => void;
|
|
@@ -68,4 +84,4 @@ declare function useTranslation(): TranslationResult;
|
|
|
68
84
|
|
|
69
85
|
declare const ExperienceContext: React.Context<Experience | null>;
|
|
70
86
|
|
|
71
|
-
export { ErrorBoundary, ExperienceContext, ExperienceProvider, type ExperienceProviderProps, useExperience, useFlag, useTrack, useTranslation };
|
|
87
|
+
export { ErrorBoundary, ExperienceContext, ExperienceProvider, type ExperienceProviderProps, FeedbackButton, useExperience, useFlag, useTrack, useTranslation };
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,22 @@ declare class ErrorBoundary extends React__default.Component<ErrorBoundaryProps,
|
|
|
35
35
|
render(): string | number | boolean | Iterable<React__default.ReactNode> | react_jsx_runtime.JSX.Element | null | undefined;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
interface FeedbackButtonProps {
|
|
39
|
+
/** Position of the floating button */
|
|
40
|
+
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
|
|
41
|
+
/** Custom button label */
|
|
42
|
+
label?: string;
|
|
43
|
+
/** Button colour */
|
|
44
|
+
color?: string;
|
|
45
|
+
/** Hide the floating button (use exp.openFeedback() instead) */
|
|
46
|
+
hidden?: boolean;
|
|
47
|
+
/** API endpoint base URL */
|
|
48
|
+
apiUrl?: string;
|
|
49
|
+
/** Auth token for user endpoints */
|
|
50
|
+
authToken?: string;
|
|
51
|
+
}
|
|
52
|
+
declare function FeedbackButton({ position, label, color, hidden, apiUrl, authToken, }: FeedbackButtonProps): react_jsx_runtime.JSX.Element;
|
|
53
|
+
|
|
38
54
|
declare function useExperience(): _shellapps_experience.Experience;
|
|
39
55
|
declare function useTrack(): {
|
|
40
56
|
track: (eventName: string, metadata?: Record<string, string>) => void;
|
|
@@ -68,4 +84,4 @@ declare function useTranslation(): TranslationResult;
|
|
|
68
84
|
|
|
69
85
|
declare const ExperienceContext: React.Context<Experience | null>;
|
|
70
86
|
|
|
71
|
-
export { ErrorBoundary, ExperienceContext, ExperienceProvider, type ExperienceProviderProps, useExperience, useFlag, useTrack, useTranslation };
|
|
87
|
+
export { ErrorBoundary, ExperienceContext, ExperienceProvider, type ExperienceProviderProps, FeedbackButton, useExperience, useFlag, useTrack, useTranslation };
|
package/dist/index.js
CHANGED
|
@@ -33,6 +33,7 @@ __export(index_exports, {
|
|
|
33
33
|
ErrorBoundary: () => ErrorBoundary,
|
|
34
34
|
ExperienceContext: () => ExperienceContext,
|
|
35
35
|
ExperienceProvider: () => ExperienceProvider,
|
|
36
|
+
FeedbackButton: () => FeedbackButton,
|
|
36
37
|
useExperience: () => useExperience,
|
|
37
38
|
useFlag: () => useFlag,
|
|
38
39
|
useTrack: () => useTrack,
|
|
@@ -145,16 +146,533 @@ var ErrorBoundary = class extends import_react3.default.Component {
|
|
|
145
146
|
};
|
|
146
147
|
ErrorBoundary.contextType = ExperienceContext;
|
|
147
148
|
|
|
148
|
-
// src/
|
|
149
|
+
// src/FeedbackButton.tsx
|
|
149
150
|
var import_react4 = require("react");
|
|
151
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
152
|
+
var TYPE_META = {
|
|
153
|
+
"bug": { icon: "\u{1F41B}", label: "Bug Report", color: "#ef4444" },
|
|
154
|
+
"feature-request": { icon: "\u{1F4A1}", label: "Feature Request", color: "#8b5cf6" },
|
|
155
|
+
"comment": { icon: "\u{1F4AC}", label: "Comment", color: "#3b82f6" },
|
|
156
|
+
"praise": { icon: "\u{1F389}", label: "Praise", color: "#10b981" }
|
|
157
|
+
};
|
|
158
|
+
var STATUS_LABELS = {
|
|
159
|
+
"new": "\u{1F195} New",
|
|
160
|
+
"acknowledged": "\u{1F440} Acknowledged",
|
|
161
|
+
"in-progress": "\u{1F527} In Progress",
|
|
162
|
+
"resolved": "\u2705 Resolved",
|
|
163
|
+
"wont-fix": "\u{1F6AB} Won't Fix"
|
|
164
|
+
};
|
|
165
|
+
function FeedbackButton({
|
|
166
|
+
position = "bottom-right",
|
|
167
|
+
label = "Feedback",
|
|
168
|
+
color = "#6366f1",
|
|
169
|
+
hidden = false,
|
|
170
|
+
apiUrl,
|
|
171
|
+
authToken
|
|
172
|
+
}) {
|
|
173
|
+
const experience = (0, import_react4.useContext)(ExperienceContext);
|
|
174
|
+
const [isOpen, setIsOpen] = (0, import_react4.useState)(false);
|
|
175
|
+
const [view, setView] = (0, import_react4.useState)("list");
|
|
176
|
+
const [selectedType, setSelectedType] = (0, import_react4.useState)("comment");
|
|
177
|
+
const [title, setTitle] = (0, import_react4.useState)("");
|
|
178
|
+
const [content, setContent] = (0, import_react4.useState)("");
|
|
179
|
+
const [submitting, setSubmitting] = (0, import_react4.useState)(false);
|
|
180
|
+
const [items, setItems] = (0, import_react4.useState)([]);
|
|
181
|
+
const [selectedItem, setSelectedItem] = (0, import_react4.useState)(null);
|
|
182
|
+
const [replies, setReplies] = (0, import_react4.useState)([]);
|
|
183
|
+
const [replyText, setReplyText] = (0, import_react4.useState)("");
|
|
184
|
+
const [loading, setLoading] = (0, import_react4.useState)(false);
|
|
185
|
+
const [screenshot, setScreenshot] = (0, import_react4.useState)(null);
|
|
186
|
+
const panelRef = (0, import_react4.useRef)(null);
|
|
187
|
+
const baseUrl = apiUrl || experience?.config?.apiUrl || "https://experience.shellapps.com";
|
|
188
|
+
const appId = experience?.config?.appId || "";
|
|
189
|
+
const headers = (0, import_react4.useCallback)(() => {
|
|
190
|
+
const h = { "Content-Type": "application/json" };
|
|
191
|
+
if (authToken) h["Authorization"] = `Bearer ${authToken}`;
|
|
192
|
+
return h;
|
|
193
|
+
}, [authToken]);
|
|
194
|
+
(0, import_react4.useEffect)(() => {
|
|
195
|
+
if (experience) {
|
|
196
|
+
experience.openFeedback = () => setIsOpen(true);
|
|
197
|
+
}
|
|
198
|
+
return () => {
|
|
199
|
+
if (experience) delete experience.openFeedback;
|
|
200
|
+
};
|
|
201
|
+
}, [experience]);
|
|
202
|
+
(0, import_react4.useEffect)(() => {
|
|
203
|
+
if (isOpen && view === "list") {
|
|
204
|
+
loadFeedback();
|
|
205
|
+
}
|
|
206
|
+
}, [isOpen, view]);
|
|
207
|
+
const loadFeedback = async () => {
|
|
208
|
+
setLoading(true);
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(
|
|
211
|
+
`${baseUrl}/api/v1/feedback?appId=${appId}&limit=20`,
|
|
212
|
+
{ headers: headers() }
|
|
213
|
+
);
|
|
214
|
+
if (res.ok) {
|
|
215
|
+
const data = await res.json();
|
|
216
|
+
setItems(data.items || []);
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
setLoading(false);
|
|
221
|
+
};
|
|
222
|
+
const loadDetail = async (item) => {
|
|
223
|
+
setSelectedItem(item);
|
|
224
|
+
setView("detail");
|
|
225
|
+
try {
|
|
226
|
+
const res = await fetch(
|
|
227
|
+
`${baseUrl}/api/v1/feedback/${item._id}`,
|
|
228
|
+
{ headers: headers() }
|
|
229
|
+
);
|
|
230
|
+
if (res.ok) {
|
|
231
|
+
const data = await res.json();
|
|
232
|
+
setReplies(data.replies || []);
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const captureScreenshot = async () => {
|
|
238
|
+
try {
|
|
239
|
+
const html2canvas = window.html2canvas;
|
|
240
|
+
if (!html2canvas) return;
|
|
241
|
+
const canvas = await html2canvas(document.body, { scale: 0.5, logging: false });
|
|
242
|
+
setScreenshot(canvas.toDataURL("image/jpeg", 0.6));
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const submit = async () => {
|
|
247
|
+
if (!title.trim() || !content.trim()) return;
|
|
248
|
+
setSubmitting(true);
|
|
249
|
+
try {
|
|
250
|
+
const ctx = {
|
|
251
|
+
userAgent: navigator.userAgent,
|
|
252
|
+
url: window.location.href,
|
|
253
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
254
|
+
locale: navigator.language,
|
|
255
|
+
referrer: document.referrer || void 0
|
|
256
|
+
};
|
|
257
|
+
const res = await fetch(`${baseUrl}/api/v1/feedback`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: headers(),
|
|
260
|
+
body: JSON.stringify({
|
|
261
|
+
appId,
|
|
262
|
+
type: selectedType,
|
|
263
|
+
title: title.trim(),
|
|
264
|
+
content: content.trim(),
|
|
265
|
+
context: ctx,
|
|
266
|
+
screenshot: screenshot || void 0
|
|
267
|
+
})
|
|
268
|
+
});
|
|
269
|
+
if (res.ok) {
|
|
270
|
+
setTitle("");
|
|
271
|
+
setContent("");
|
|
272
|
+
setScreenshot(null);
|
|
273
|
+
setView("list");
|
|
274
|
+
loadFeedback();
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
278
|
+
setSubmitting(false);
|
|
279
|
+
};
|
|
280
|
+
const submitReply = async () => {
|
|
281
|
+
if (!replyText.trim() || !selectedItem) return;
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(`${baseUrl}/api/v1/feedback/${selectedItem._id}/reply`, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: headers(),
|
|
286
|
+
body: JSON.stringify({ content: replyText.trim() })
|
|
287
|
+
});
|
|
288
|
+
if (res.ok) {
|
|
289
|
+
const reply = await res.json();
|
|
290
|
+
setReplies((prev) => [...prev, reply]);
|
|
291
|
+
setReplyText("");
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const vote = async (id) => {
|
|
297
|
+
try {
|
|
298
|
+
await fetch(`${baseUrl}/api/v1/feedback/${id}/vote`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: headers(),
|
|
301
|
+
body: JSON.stringify({ type: "upvote" })
|
|
302
|
+
});
|
|
303
|
+
loadFeedback();
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const posStyles = {
|
|
308
|
+
"bottom-right": { bottom: 20, right: 20 },
|
|
309
|
+
"bottom-left": { bottom: 20, left: 20 },
|
|
310
|
+
"top-right": { top: 20, right: 20 },
|
|
311
|
+
"top-left": { top: 20, left: 20 }
|
|
312
|
+
};
|
|
313
|
+
const panelPos = {
|
|
314
|
+
"bottom-right": { bottom: 70, right: 20 },
|
|
315
|
+
"bottom-left": { bottom: 70, left: 20 },
|
|
316
|
+
"top-right": { top: 70, right: 20 },
|
|
317
|
+
"top-left": { top: 70, left: 20 }
|
|
318
|
+
};
|
|
319
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
320
|
+
!hidden && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
321
|
+
"button",
|
|
322
|
+
{
|
|
323
|
+
onClick: () => setIsOpen(!isOpen),
|
|
324
|
+
style: {
|
|
325
|
+
position: "fixed",
|
|
326
|
+
...posStyles[position],
|
|
327
|
+
zIndex: 99999,
|
|
328
|
+
background: color,
|
|
329
|
+
color: "#fff",
|
|
330
|
+
border: "none",
|
|
331
|
+
borderRadius: 28,
|
|
332
|
+
padding: "10px 20px",
|
|
333
|
+
fontSize: 14,
|
|
334
|
+
fontWeight: 600,
|
|
335
|
+
cursor: "pointer",
|
|
336
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
|
337
|
+
display: "flex",
|
|
338
|
+
alignItems: "center",
|
|
339
|
+
gap: 6
|
|
340
|
+
},
|
|
341
|
+
children: [
|
|
342
|
+
"\u{1F4AC} ",
|
|
343
|
+
label
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
isOpen && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
348
|
+
"div",
|
|
349
|
+
{
|
|
350
|
+
ref: panelRef,
|
|
351
|
+
style: {
|
|
352
|
+
position: "fixed",
|
|
353
|
+
...panelPos[position],
|
|
354
|
+
zIndex: 1e5,
|
|
355
|
+
width: 380,
|
|
356
|
+
maxHeight: 520,
|
|
357
|
+
background: "#fff",
|
|
358
|
+
borderRadius: 12,
|
|
359
|
+
boxShadow: "0 8px 32px rgba(0,0,0,0.18)",
|
|
360
|
+
display: "flex",
|
|
361
|
+
flexDirection: "column",
|
|
362
|
+
overflow: "hidden",
|
|
363
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
364
|
+
fontSize: 14,
|
|
365
|
+
color: "#1f2937"
|
|
366
|
+
},
|
|
367
|
+
children: [
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
369
|
+
"div",
|
|
370
|
+
{
|
|
371
|
+
style: {
|
|
372
|
+
padding: "14px 16px",
|
|
373
|
+
background: color,
|
|
374
|
+
color: "#fff",
|
|
375
|
+
display: "flex",
|
|
376
|
+
justifyContent: "space-between",
|
|
377
|
+
alignItems: "center"
|
|
378
|
+
},
|
|
379
|
+
children: [
|
|
380
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { fontWeight: 600 }, children: view === "new" ? "New Feedback" : view === "detail" ? "Feedback" : "Feedback" }),
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 8 }, children: [
|
|
382
|
+
view !== "list" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
383
|
+
"button",
|
|
384
|
+
{
|
|
385
|
+
onClick: () => {
|
|
386
|
+
setView("list");
|
|
387
|
+
setSelectedItem(null);
|
|
388
|
+
},
|
|
389
|
+
style: { background: "none", border: "none", color: "#fff", cursor: "pointer", fontSize: 14 },
|
|
390
|
+
children: "\u2190 Back"
|
|
391
|
+
}
|
|
392
|
+
),
|
|
393
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
394
|
+
"button",
|
|
395
|
+
{
|
|
396
|
+
onClick: () => setIsOpen(false),
|
|
397
|
+
style: { background: "none", border: "none", color: "#fff", cursor: "pointer", fontSize: 18 },
|
|
398
|
+
children: "\u2715"
|
|
399
|
+
}
|
|
400
|
+
)
|
|
401
|
+
] })
|
|
402
|
+
]
|
|
403
|
+
}
|
|
404
|
+
),
|
|
405
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1, overflow: "auto", padding: 16 }, children: [
|
|
406
|
+
view === "list" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
407
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
408
|
+
"button",
|
|
409
|
+
{
|
|
410
|
+
onClick: () => setView("new"),
|
|
411
|
+
style: {
|
|
412
|
+
width: "100%",
|
|
413
|
+
padding: "10px",
|
|
414
|
+
background: color,
|
|
415
|
+
color: "#fff",
|
|
416
|
+
border: "none",
|
|
417
|
+
borderRadius: 8,
|
|
418
|
+
cursor: "pointer",
|
|
419
|
+
fontWeight: 600,
|
|
420
|
+
marginBottom: 12
|
|
421
|
+
},
|
|
422
|
+
children: "+ New Feedback"
|
|
423
|
+
}
|
|
424
|
+
),
|
|
425
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { textAlign: "center", color: "#9ca3af" }, children: "Loading\u2026" }) : items.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { textAlign: "center", color: "#9ca3af" }, children: "No feedback yet" }) : items.map((item) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
426
|
+
"div",
|
|
427
|
+
{
|
|
428
|
+
onClick: () => loadDetail(item),
|
|
429
|
+
style: {
|
|
430
|
+
padding: 10,
|
|
431
|
+
borderRadius: 8,
|
|
432
|
+
border: "1px solid #e5e7eb",
|
|
433
|
+
marginBottom: 8,
|
|
434
|
+
cursor: "pointer"
|
|
435
|
+
},
|
|
436
|
+
children: [
|
|
437
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", justifyContent: "space-between", marginBottom: 4 }, children: [
|
|
438
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
|
439
|
+
TYPE_META[item.type]?.icon,
|
|
440
|
+
" ",
|
|
441
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: item.title })
|
|
442
|
+
] }),
|
|
443
|
+
item.type === "feature-request" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
444
|
+
"button",
|
|
445
|
+
{
|
|
446
|
+
onClick: (e) => {
|
|
447
|
+
e.stopPropagation();
|
|
448
|
+
vote(item._id);
|
|
449
|
+
},
|
|
450
|
+
style: {
|
|
451
|
+
background: "none",
|
|
452
|
+
border: "1px solid #d1d5db",
|
|
453
|
+
borderRadius: 4,
|
|
454
|
+
padding: "2px 6px",
|
|
455
|
+
cursor: "pointer",
|
|
456
|
+
fontSize: 12
|
|
457
|
+
},
|
|
458
|
+
children: [
|
|
459
|
+
"\u25B2 ",
|
|
460
|
+
item.vote_count
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
)
|
|
464
|
+
] }),
|
|
465
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 12, color: "#6b7280" }, children: [
|
|
466
|
+
STATUS_LABELS[item.status],
|
|
467
|
+
" \xB7 ",
|
|
468
|
+
item.reply_count,
|
|
469
|
+
" replies \xB7 ",
|
|
470
|
+
new Date(item.created_at).toLocaleDateString()
|
|
471
|
+
] })
|
|
472
|
+
]
|
|
473
|
+
},
|
|
474
|
+
item._id
|
|
475
|
+
))
|
|
476
|
+
] }),
|
|
477
|
+
view === "new" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
478
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8, marginBottom: 12 }, children: Object.entries(TYPE_META).map(
|
|
479
|
+
([type, meta]) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
480
|
+
"button",
|
|
481
|
+
{
|
|
482
|
+
onClick: () => setSelectedType(type),
|
|
483
|
+
style: {
|
|
484
|
+
padding: 8,
|
|
485
|
+
borderRadius: 8,
|
|
486
|
+
border: `2px solid ${selectedType === type ? meta.color : "#e5e7eb"}`,
|
|
487
|
+
background: selectedType === type ? `${meta.color}10` : "#fff",
|
|
488
|
+
cursor: "pointer",
|
|
489
|
+
fontSize: 13,
|
|
490
|
+
textAlign: "center"
|
|
491
|
+
},
|
|
492
|
+
children: [
|
|
493
|
+
meta.icon,
|
|
494
|
+
" ",
|
|
495
|
+
meta.label
|
|
496
|
+
]
|
|
497
|
+
},
|
|
498
|
+
type
|
|
499
|
+
)
|
|
500
|
+
) }),
|
|
501
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
502
|
+
"input",
|
|
503
|
+
{
|
|
504
|
+
placeholder: "Title",
|
|
505
|
+
value: title,
|
|
506
|
+
onChange: (e) => setTitle(e.target.value),
|
|
507
|
+
style: {
|
|
508
|
+
width: "100%",
|
|
509
|
+
padding: 8,
|
|
510
|
+
borderRadius: 6,
|
|
511
|
+
border: "1px solid #d1d5db",
|
|
512
|
+
marginBottom: 8,
|
|
513
|
+
fontSize: 14,
|
|
514
|
+
boxSizing: "border-box"
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
),
|
|
518
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
519
|
+
"textarea",
|
|
520
|
+
{
|
|
521
|
+
placeholder: "Describe your feedback\u2026",
|
|
522
|
+
value: content,
|
|
523
|
+
onChange: (e) => setContent(e.target.value),
|
|
524
|
+
rows: 4,
|
|
525
|
+
style: {
|
|
526
|
+
width: "100%",
|
|
527
|
+
padding: 8,
|
|
528
|
+
borderRadius: 6,
|
|
529
|
+
border: "1px solid #d1d5db",
|
|
530
|
+
marginBottom: 8,
|
|
531
|
+
fontSize: 14,
|
|
532
|
+
resize: "vertical",
|
|
533
|
+
boxSizing: "border-box"
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
),
|
|
537
|
+
window.html2canvas && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
538
|
+
"button",
|
|
539
|
+
{
|
|
540
|
+
onClick: captureScreenshot,
|
|
541
|
+
style: {
|
|
542
|
+
background: "none",
|
|
543
|
+
border: "1px solid #d1d5db",
|
|
544
|
+
borderRadius: 6,
|
|
545
|
+
padding: "6px 10px",
|
|
546
|
+
cursor: "pointer",
|
|
547
|
+
fontSize: 12,
|
|
548
|
+
marginBottom: 8,
|
|
549
|
+
color: screenshot ? "#10b981" : "#6b7280"
|
|
550
|
+
},
|
|
551
|
+
children: [
|
|
552
|
+
"\u{1F4F7} ",
|
|
553
|
+
screenshot ? "Screenshot captured \u2713" : "Capture screenshot"
|
|
554
|
+
]
|
|
555
|
+
}
|
|
556
|
+
),
|
|
557
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
558
|
+
"button",
|
|
559
|
+
{
|
|
560
|
+
onClick: submit,
|
|
561
|
+
disabled: submitting || !title.trim() || !content.trim(),
|
|
562
|
+
style: {
|
|
563
|
+
width: "100%",
|
|
564
|
+
padding: 10,
|
|
565
|
+
background: submitting ? "#9ca3af" : color,
|
|
566
|
+
color: "#fff",
|
|
567
|
+
border: "none",
|
|
568
|
+
borderRadius: 8,
|
|
569
|
+
cursor: submitting ? "default" : "pointer",
|
|
570
|
+
fontWeight: 600
|
|
571
|
+
},
|
|
572
|
+
children: submitting ? "Submitting\u2026" : "Submit"
|
|
573
|
+
}
|
|
574
|
+
)
|
|
575
|
+
] }),
|
|
576
|
+
view === "detail" && selectedItem && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
577
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: 12 }, children: [
|
|
578
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 12, color: TYPE_META[selectedItem.type]?.color, fontWeight: 600, marginBottom: 4 }, children: [
|
|
579
|
+
TYPE_META[selectedItem.type]?.icon,
|
|
580
|
+
" ",
|
|
581
|
+
TYPE_META[selectedItem.type]?.label
|
|
582
|
+
] }),
|
|
583
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { style: { margin: "0 0 4px", fontSize: 16 }, children: selectedItem.title }),
|
|
584
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 12, color: "#6b7280", marginBottom: 8 }, children: [
|
|
585
|
+
STATUS_LABELS[selectedItem.status],
|
|
586
|
+
" \xB7 ",
|
|
587
|
+
new Date(selectedItem.created_at).toLocaleDateString()
|
|
588
|
+
] }),
|
|
589
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: { margin: 0, lineHeight: 1.5 }, children: selectedItem.content })
|
|
590
|
+
] }),
|
|
591
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("hr", { style: { border: "none", borderTop: "1px solid #e5e7eb", margin: "12px 0" } }),
|
|
592
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { marginBottom: 12 }, children: [
|
|
593
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("strong", { style: { fontSize: 13 }, children: [
|
|
594
|
+
"Replies (",
|
|
595
|
+
replies.length,
|
|
596
|
+
")"
|
|
597
|
+
] }),
|
|
598
|
+
replies.map((r) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
599
|
+
"div",
|
|
600
|
+
{
|
|
601
|
+
style: {
|
|
602
|
+
padding: 8,
|
|
603
|
+
marginTop: 8,
|
|
604
|
+
borderRadius: 6,
|
|
605
|
+
background: r.author_type === "developer" ? "#eff6ff" : "#f9fafb",
|
|
606
|
+
borderLeft: `3px solid ${r.author_type === "developer" ? "#3b82f6" : "#d1d5db"}`
|
|
607
|
+
},
|
|
608
|
+
children: [
|
|
609
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: 11, color: "#6b7280", marginBottom: 2 }, children: [
|
|
610
|
+
r.author_type === "developer" ? "\u{1F6E0}\uFE0F Developer" : "\u{1F464} You",
|
|
611
|
+
r.author_name ? ` \xB7 ${r.author_name}` : "",
|
|
612
|
+
" \xB7 ",
|
|
613
|
+
new Date(r.created_at).toLocaleDateString()
|
|
614
|
+
] }),
|
|
615
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { fontSize: 13 }, children: r.content })
|
|
616
|
+
]
|
|
617
|
+
},
|
|
618
|
+
r._id
|
|
619
|
+
))
|
|
620
|
+
] }),
|
|
621
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
|
|
622
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
623
|
+
"input",
|
|
624
|
+
{
|
|
625
|
+
placeholder: "Write a reply\u2026",
|
|
626
|
+
value: replyText,
|
|
627
|
+
onChange: (e) => setReplyText(e.target.value),
|
|
628
|
+
onKeyDown: (e) => {
|
|
629
|
+
if (e.key === "Enter") submitReply();
|
|
630
|
+
},
|
|
631
|
+
style: {
|
|
632
|
+
flex: 1,
|
|
633
|
+
padding: 8,
|
|
634
|
+
borderRadius: 6,
|
|
635
|
+
border: "1px solid #d1d5db",
|
|
636
|
+
fontSize: 13
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
),
|
|
640
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
641
|
+
"button",
|
|
642
|
+
{
|
|
643
|
+
onClick: submitReply,
|
|
644
|
+
disabled: !replyText.trim(),
|
|
645
|
+
style: {
|
|
646
|
+
padding: "8px 12px",
|
|
647
|
+
background: color,
|
|
648
|
+
color: "#fff",
|
|
649
|
+
border: "none",
|
|
650
|
+
borderRadius: 6,
|
|
651
|
+
cursor: "pointer",
|
|
652
|
+
fontSize: 13
|
|
653
|
+
},
|
|
654
|
+
children: "Send"
|
|
655
|
+
}
|
|
656
|
+
)
|
|
657
|
+
] })
|
|
658
|
+
] })
|
|
659
|
+
] })
|
|
660
|
+
]
|
|
661
|
+
}
|
|
662
|
+
)
|
|
663
|
+
] });
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/hooks.ts
|
|
667
|
+
var import_react5 = require("react");
|
|
150
668
|
function useExperience() {
|
|
151
|
-
const ctx = (0,
|
|
669
|
+
const ctx = (0, import_react5.useContext)(ExperienceContext);
|
|
152
670
|
if (!ctx) throw new Error("useExperience must be used within ExperienceProvider");
|
|
153
671
|
return ctx;
|
|
154
672
|
}
|
|
155
673
|
function useTrack() {
|
|
156
674
|
const experience = useExperience();
|
|
157
|
-
return (0,
|
|
675
|
+
return (0, import_react5.useMemo)(() => ({
|
|
158
676
|
track: (eventName, metadata) => experience.track(eventName, metadata),
|
|
159
677
|
trackPageView: () => experience.trackPageView()
|
|
160
678
|
}), [experience]);
|
|
@@ -170,13 +688,13 @@ function useTranslation() {
|
|
|
170
688
|
const config = experience.config || {};
|
|
171
689
|
const appId = config.appId || "";
|
|
172
690
|
const apiBaseUrl = config.apiUrl || config.baseUrl || "https://experience-api.shellapps.com";
|
|
173
|
-
const [locale, setLocaleState] = (0,
|
|
691
|
+
const [locale, setLocaleState] = (0, import_react5.useState)(() => {
|
|
174
692
|
if (typeof window === "undefined") return "en";
|
|
175
693
|
const saved = localStorage.getItem(`exp_locale_${appId}`);
|
|
176
694
|
if (saved) return saved;
|
|
177
695
|
return navigator.language?.split("-")[0] || "en";
|
|
178
696
|
});
|
|
179
|
-
const [translations, setTranslations] = (0,
|
|
697
|
+
const [translations, setTranslations] = (0, import_react5.useState)(() => {
|
|
180
698
|
if (translationCache[locale]) return translationCache[locale];
|
|
181
699
|
if (typeof window !== "undefined") {
|
|
182
700
|
try {
|
|
@@ -191,9 +709,9 @@ function useTranslation() {
|
|
|
191
709
|
}
|
|
192
710
|
return {};
|
|
193
711
|
});
|
|
194
|
-
const [locales, setLocales] = (0,
|
|
195
|
-
const [isLoading, setIsLoading] = (0,
|
|
196
|
-
const fetchTranslations = (0,
|
|
712
|
+
const [locales, setLocales] = (0, import_react5.useState)([]);
|
|
713
|
+
const [isLoading, setIsLoading] = (0, import_react5.useState)(false);
|
|
714
|
+
const fetchTranslations = (0, import_react5.useCallback)(async (loc) => {
|
|
197
715
|
if (!appId) return {};
|
|
198
716
|
const cacheKey = `${appId}_${loc}`;
|
|
199
717
|
const existing = fetchPromises.get(cacheKey);
|
|
@@ -220,12 +738,12 @@ function useTranslation() {
|
|
|
220
738
|
fetchPromises.set(cacheKey, promise);
|
|
221
739
|
return promise;
|
|
222
740
|
}, [appId, apiBaseUrl]);
|
|
223
|
-
(0,
|
|
741
|
+
(0, import_react5.useEffect)(() => {
|
|
224
742
|
if (!appId) return;
|
|
225
743
|
fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`).then((res) => res.ok ? res.json() : { locales: [] }).then((data) => setLocales(data.locales || [])).catch(() => {
|
|
226
744
|
});
|
|
227
745
|
}, [appId, apiBaseUrl]);
|
|
228
|
-
(0,
|
|
746
|
+
(0, import_react5.useEffect)(() => {
|
|
229
747
|
if (!appId) return;
|
|
230
748
|
if (translationCache[locale]) {
|
|
231
749
|
setTranslations(translationCache[locale]);
|
|
@@ -237,7 +755,7 @@ function useTranslation() {
|
|
|
237
755
|
setIsLoading(false);
|
|
238
756
|
});
|
|
239
757
|
}, [locale, appId, fetchTranslations]);
|
|
240
|
-
const t = (0,
|
|
758
|
+
const t = (0, import_react5.useCallback)((key, params) => {
|
|
241
759
|
let result = translations[key] ?? key;
|
|
242
760
|
if (result.includes("||") && params && "count" in params) {
|
|
243
761
|
const parts = result.split("||");
|
|
@@ -251,7 +769,7 @@ function useTranslation() {
|
|
|
251
769
|
}
|
|
252
770
|
return result;
|
|
253
771
|
}, [translations]);
|
|
254
|
-
const setLocale = (0,
|
|
772
|
+
const setLocale = (0, import_react5.useCallback)((newLocale) => {
|
|
255
773
|
setLocaleState(newLocale);
|
|
256
774
|
if (typeof window !== "undefined") {
|
|
257
775
|
localStorage.setItem(`exp_locale_${appId}`, newLocale);
|
|
@@ -264,6 +782,7 @@ function useTranslation() {
|
|
|
264
782
|
ErrorBoundary,
|
|
265
783
|
ExperienceContext,
|
|
266
784
|
ExperienceProvider,
|
|
785
|
+
FeedbackButton,
|
|
267
786
|
useExperience,
|
|
268
787
|
useFlag,
|
|
269
788
|
useTrack,
|