@shellapps/experience-react 1.2.0 → 1.3.1

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