@shellapps/experience-react 1.1.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 +29 -4
- package/dist/index.d.ts +29 -4
- package/dist/index.js +609 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +607 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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]);
|
|
@@ -163,29 +681,108 @@ function useFlag(flagName, defaultValue) {
|
|
|
163
681
|
const experience = useExperience();
|
|
164
682
|
return experience.getFlag(flagName, defaultValue);
|
|
165
683
|
}
|
|
684
|
+
var translationCache = {};
|
|
685
|
+
var fetchPromises = /* @__PURE__ */ new Map();
|
|
166
686
|
function useTranslation() {
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
687
|
+
const experience = useExperience();
|
|
688
|
+
const config = experience.config || {};
|
|
689
|
+
const appId = config.appId || "";
|
|
690
|
+
const apiBaseUrl = config.apiUrl || config.baseUrl || "https://experience-api.shellapps.com";
|
|
691
|
+
const [locale, setLocaleState] = (0, import_react5.useState)(() => {
|
|
692
|
+
if (typeof window === "undefined") return "en";
|
|
693
|
+
const saved = localStorage.getItem(`exp_locale_${appId}`);
|
|
694
|
+
if (saved) return saved;
|
|
695
|
+
return navigator.language?.split("-")[0] || "en";
|
|
696
|
+
});
|
|
697
|
+
const [translations, setTranslations] = (0, import_react5.useState)(() => {
|
|
698
|
+
if (translationCache[locale]) return translationCache[locale];
|
|
699
|
+
if (typeof window !== "undefined") {
|
|
700
|
+
try {
|
|
701
|
+
const cached = localStorage.getItem(`exp_translations_${appId}_${locale}`);
|
|
702
|
+
if (cached) {
|
|
703
|
+
const parsed = JSON.parse(cached);
|
|
704
|
+
translationCache[locale] = parsed;
|
|
705
|
+
return parsed;
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return {};
|
|
711
|
+
});
|
|
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) => {
|
|
715
|
+
if (!appId) return {};
|
|
716
|
+
const cacheKey = `${appId}_${loc}`;
|
|
717
|
+
const existing = fetchPromises.get(cacheKey);
|
|
718
|
+
if (existing) return existing;
|
|
719
|
+
const promise = (async () => {
|
|
720
|
+
try {
|
|
721
|
+
const res = await fetch(`${apiBaseUrl}/api/v1/translations/${appId}/${loc}`);
|
|
722
|
+
if (!res.ok) return {};
|
|
723
|
+
const data = await res.json();
|
|
724
|
+
translationCache[loc] = data;
|
|
725
|
+
if (typeof window !== "undefined") {
|
|
726
|
+
try {
|
|
727
|
+
localStorage.setItem(`exp_translations_${appId}_${loc}`, JSON.stringify(data));
|
|
728
|
+
} catch {
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return data;
|
|
732
|
+
} catch {
|
|
733
|
+
return {};
|
|
734
|
+
} finally {
|
|
735
|
+
fetchPromises.delete(cacheKey);
|
|
736
|
+
}
|
|
737
|
+
})();
|
|
738
|
+
fetchPromises.set(cacheKey, promise);
|
|
739
|
+
return promise;
|
|
740
|
+
}, [appId, apiBaseUrl]);
|
|
741
|
+
(0, import_react5.useEffect)(() => {
|
|
742
|
+
if (!appId) return;
|
|
743
|
+
fetch(`${apiBaseUrl}/api/v1/translations/${appId}/locales`).then((res) => res.ok ? res.json() : { locales: [] }).then((data) => setLocales(data.locales || [])).catch(() => {
|
|
744
|
+
});
|
|
745
|
+
}, [appId, apiBaseUrl]);
|
|
746
|
+
(0, import_react5.useEffect)(() => {
|
|
747
|
+
if (!appId) return;
|
|
748
|
+
if (translationCache[locale]) {
|
|
749
|
+
setTranslations(translationCache[locale]);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
setIsLoading(true);
|
|
753
|
+
fetchTranslations(locale).then((data) => {
|
|
754
|
+
setTranslations(data);
|
|
755
|
+
setIsLoading(false);
|
|
756
|
+
});
|
|
757
|
+
}, [locale, appId, fetchTranslations]);
|
|
758
|
+
const t = (0, import_react5.useCallback)((key, params) => {
|
|
759
|
+
let result = translations[key] ?? key;
|
|
760
|
+
if (result.includes("||") && params && "count" in params) {
|
|
761
|
+
const parts = result.split("||");
|
|
762
|
+
const count = Number(params.count);
|
|
763
|
+
result = count === 1 ? parts[0] ?? result : parts[1] ?? result;
|
|
764
|
+
}
|
|
172
765
|
if (params) {
|
|
173
766
|
for (const [k, v] of Object.entries(params)) {
|
|
174
767
|
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
|
175
768
|
}
|
|
176
769
|
}
|
|
177
770
|
return result;
|
|
178
|
-
}, []);
|
|
179
|
-
const setLocale = (0,
|
|
771
|
+
}, [translations]);
|
|
772
|
+
const setLocale = (0, import_react5.useCallback)((newLocale) => {
|
|
180
773
|
setLocaleState(newLocale);
|
|
181
|
-
|
|
182
|
-
|
|
774
|
+
if (typeof window !== "undefined") {
|
|
775
|
+
localStorage.setItem(`exp_locale_${appId}`, newLocale);
|
|
776
|
+
}
|
|
777
|
+
}, [appId]);
|
|
778
|
+
return { t, locale, setLocale, locales, isLoading };
|
|
183
779
|
}
|
|
184
780
|
// Annotate the CommonJS export names for ESM import in node:
|
|
185
781
|
0 && (module.exports = {
|
|
186
782
|
ErrorBoundary,
|
|
187
783
|
ExperienceContext,
|
|
188
784
|
ExperienceProvider,
|
|
785
|
+
FeedbackButton,
|
|
189
786
|
useExperience,
|
|
190
787
|
useFlag,
|
|
191
788
|
useTrack,
|