@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.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/hooks.ts
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, import_react4.useContext)(ExperienceContext);
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, import_react4.useMemo)(() => ({
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 [locale, setLocaleState] = (0, import_react4.useState)(
168
- typeof navigator !== "undefined" ? navigator.language?.split("-")[0] || "en" : "en"
169
- );
170
- const t = (0, import_react4.useCallback)((key, params) => {
171
- let result = key;
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, import_react4.useCallback)((newLocale) => {
771
+ }, [translations]);
772
+ const setLocale = (0, import_react5.useCallback)((newLocale) => {
180
773
  setLocaleState(newLocale);
181
- }, []);
182
- return { t, locale, setLocale, locales: [] };
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,