@usero/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/react.cjs +1137 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +72 -0
- package/dist/react.d.ts +72 -0
- package/dist/react.js +1132 -0
- package/dist/react.js.map +1 -0
- package/dist/usero.iife.js +471 -0
- package/dist/usero.iife.js.map +1 -0
- package/dist/vanilla.cjs +1071 -0
- package/dist/vanilla.cjs.map +1 -0
- package/dist/vanilla.d.cts +71 -0
- package/dist/vanilla.d.ts +71 -0
- package/dist/vanilla.js +1066 -0
- package/dist/vanilla.js.map +1 -0
- package/package.json +80 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/react.tsx
|
|
6
|
+
|
|
7
|
+
// src/types.ts
|
|
8
|
+
var EMOJI_MAP = {
|
|
9
|
+
1: "\u{1F61E}",
|
|
10
|
+
2: "\u{1F610}",
|
|
11
|
+
3: "\u{1F60A}",
|
|
12
|
+
4: "\u{1F929}"
|
|
13
|
+
};
|
|
14
|
+
var RATING_LABELS = {
|
|
15
|
+
1: "Needs work",
|
|
16
|
+
2: "It's okay",
|
|
17
|
+
3: "Pretty good",
|
|
18
|
+
4: "Amazing!"
|
|
19
|
+
};
|
|
20
|
+
var EMOJI_BACKGROUNDS = {
|
|
21
|
+
1: "linear-gradient(135deg,#ff6b6b14,#ff6b6b1f)",
|
|
22
|
+
2: "linear-gradient(135deg,#9ca3af0f,#9ca3af1a)",
|
|
23
|
+
3: "linear-gradient(135deg,#3b82f614,#3b82f61f)",
|
|
24
|
+
4: "linear-gradient(135deg,#f59e0b14,#f59e0b1f)"
|
|
25
|
+
};
|
|
26
|
+
var DEFAULT_API_URL = "https://usero.io";
|
|
27
|
+
var DEFAULT_THEME = {
|
|
28
|
+
primary: "#2563eb",
|
|
29
|
+
background: "#ffffff",
|
|
30
|
+
text: "#374151",
|
|
31
|
+
border: "#e5e7eb",
|
|
32
|
+
shadow: "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)"
|
|
33
|
+
};
|
|
34
|
+
var DARK_THEME = {
|
|
35
|
+
primary: "#2563eb",
|
|
36
|
+
background: "#1f2937",
|
|
37
|
+
text: "#f9fafb",
|
|
38
|
+
border: "#374151",
|
|
39
|
+
shadow: "0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2)"
|
|
40
|
+
};
|
|
41
|
+
function mergeTheme(customTheme = {}) {
|
|
42
|
+
return { ...DEFAULT_THEME, ...customTheme };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/api.ts
|
|
46
|
+
function isJsonErrorBody(value) {
|
|
47
|
+
return typeof value === "object" && value !== null && "error" in value;
|
|
48
|
+
}
|
|
49
|
+
function parseScreenshotUploadBody(value) {
|
|
50
|
+
if (typeof value !== "object" || value === null) {
|
|
51
|
+
return { success: false, error: "Invalid response" };
|
|
52
|
+
}
|
|
53
|
+
const obj = value;
|
|
54
|
+
const success = obj.success === true;
|
|
55
|
+
const error = typeof obj.error === "string" ? obj.error : void 0;
|
|
56
|
+
const rawShot = obj.screenshot;
|
|
57
|
+
let screenshot;
|
|
58
|
+
if (typeof rawShot === "object" && rawShot !== null) {
|
|
59
|
+
const s = rawShot;
|
|
60
|
+
if (typeof s.fileName === "string" && typeof s.url === "string" && typeof s.fileSize === "number" && typeof s.mimeType === "string") {
|
|
61
|
+
screenshot = {
|
|
62
|
+
fileName: s.fileName,
|
|
63
|
+
url: s.url,
|
|
64
|
+
fileSize: s.fileSize,
|
|
65
|
+
mimeType: s.mimeType,
|
|
66
|
+
width: typeof s.width === "number" ? s.width : void 0,
|
|
67
|
+
height: typeof s.height === "number" ? s.height : void 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { success, error, screenshot };
|
|
72
|
+
}
|
|
73
|
+
var FeedbackApiClient = class {
|
|
74
|
+
constructor(baseUrl = DEFAULT_API_URL) {
|
|
75
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
76
|
+
}
|
|
77
|
+
async submitFeedback(data) {
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch(`${this.baseUrl}/api/feedback`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: {
|
|
82
|
+
"Content-Type": "application/json",
|
|
83
|
+
Accept: "application/json"
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify(data),
|
|
86
|
+
signal: AbortSignal.timeout(1e4)
|
|
87
|
+
});
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
|
90
|
+
try {
|
|
91
|
+
const errorData = await response.json();
|
|
92
|
+
if (isJsonErrorBody(errorData) && typeof errorData.error === "string") {
|
|
93
|
+
errorMessage = errorData.error;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
throw new Error(errorMessage);
|
|
98
|
+
}
|
|
99
|
+
const result = await response.json();
|
|
100
|
+
const message = typeof result === "object" && result !== null && "message" in result && typeof result.message === "string" ? result.message : "Feedback submitted successfully";
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
data: result,
|
|
104
|
+
message
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: error instanceof Error ? error.message : "An unexpected error occurred"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async uploadScreenshot(file, clientId) {
|
|
114
|
+
const formData = new FormData();
|
|
115
|
+
formData.append("screenshot", file);
|
|
116
|
+
formData.append("clientId", clientId);
|
|
117
|
+
const response = await fetch(`${this.baseUrl}/api/screenshots`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
body: formData,
|
|
120
|
+
signal: AbortSignal.timeout(3e4)
|
|
121
|
+
});
|
|
122
|
+
let body = { success: false };
|
|
123
|
+
try {
|
|
124
|
+
const raw = await response.json();
|
|
125
|
+
body = parseScreenshotUploadBody(raw);
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
if (!response.ok || !body.success || !body.screenshot) {
|
|
129
|
+
const message = body.error ?? `HTTP ${response.status}: ${response.statusText}`;
|
|
130
|
+
throw new Error(message);
|
|
131
|
+
}
|
|
132
|
+
return body.screenshot;
|
|
133
|
+
}
|
|
134
|
+
ping() {
|
|
135
|
+
fetch(`${this.baseUrl}/api/ping`, {
|
|
136
|
+
signal: AbortSignal.timeout(5e3)
|
|
137
|
+
}).catch(() => {
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/colorUtils.ts
|
|
143
|
+
function colorNameToHex(color) {
|
|
144
|
+
if (color.startsWith("#")) return color;
|
|
145
|
+
if (typeof document === "undefined") return color;
|
|
146
|
+
const canvas = document.createElement("canvas");
|
|
147
|
+
const ctx = canvas.getContext("2d");
|
|
148
|
+
if (!ctx) return color;
|
|
149
|
+
ctx.fillStyle = color;
|
|
150
|
+
return ctx.fillStyle;
|
|
151
|
+
}
|
|
152
|
+
function getGradientEnd(color) {
|
|
153
|
+
const hex = colorNameToHex(color);
|
|
154
|
+
if (!hex.startsWith("#") || hex.length < 7) return hex;
|
|
155
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
156
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
157
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
158
|
+
const shiftedR = Math.max(0, r - 60);
|
|
159
|
+
const shiftedG = Math.min(255, g + 40);
|
|
160
|
+
const shiftedB = Math.min(255, b + 20);
|
|
161
|
+
return `#${[shiftedR, shiftedG, shiftedB].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/validation.ts
|
|
165
|
+
function validateFeedbackSubmission(data) {
|
|
166
|
+
const errors = [];
|
|
167
|
+
const hasRating = data.rating != null;
|
|
168
|
+
const hasComment = !!data.comment?.trim();
|
|
169
|
+
if (!hasRating && !hasComment) {
|
|
170
|
+
errors.push("Add rating or comment");
|
|
171
|
+
}
|
|
172
|
+
if (hasRating && data.rating !== void 0 && ![1, 2, 3, 4].includes(data.rating)) {
|
|
173
|
+
errors.push("Invalid rating");
|
|
174
|
+
}
|
|
175
|
+
if (hasComment && data.comment !== void 0) {
|
|
176
|
+
if (data.comment.length > 1e3) {
|
|
177
|
+
errors.push("Comment too long");
|
|
178
|
+
}
|
|
179
|
+
if (/<script[^>]*>.*?<\/script>/gi.test(data.comment)) {
|
|
180
|
+
errors.push("Invalid comment");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
isValid: errors.length === 0,
|
|
185
|
+
errors
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/widgetCss.ts
|
|
190
|
+
var FEEDBACK_CSS = `
|
|
191
|
+
@keyframes spin {
|
|
192
|
+
0% { transform: rotate(0deg); }
|
|
193
|
+
100% { transform: rotate(360deg); }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.fb-es {
|
|
197
|
+
display: flex;
|
|
198
|
+
justify-content: center;
|
|
199
|
+
gap: 15px;
|
|
200
|
+
padding-bottom: 10px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.fb-ec {
|
|
204
|
+
border-radius: 16px;
|
|
205
|
+
padding: 0 5px;
|
|
206
|
+
transition: all 300ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
207
|
+
border: 3px solid transparent;
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
text-align: center;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.fb-ec--sel {
|
|
213
|
+
border-color: #2563eb;
|
|
214
|
+
transform: scale(1.05);
|
|
215
|
+
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.fb-ec--hov:not(.fb-ec--sel) {
|
|
219
|
+
transform: scale(1.05);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.fb-eb {
|
|
223
|
+
background: transparent;
|
|
224
|
+
border: none;
|
|
225
|
+
cursor: pointer;
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 2px;
|
|
230
|
+
width: 100%;
|
|
231
|
+
padding: 0;
|
|
232
|
+
transition: all 200ms ease;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.fb-ei {
|
|
236
|
+
font-size: 36px;
|
|
237
|
+
transition: transform 200ms ease;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.fb-ei--hov {
|
|
241
|
+
transform: scale(1.1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.fb-el {
|
|
245
|
+
font-size: 13px;
|
|
246
|
+
font-weight: 600;
|
|
247
|
+
color: currentColor;
|
|
248
|
+
line-height: 1.2;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.fb-hdr {
|
|
252
|
+
display: flex;
|
|
253
|
+
justify-content: space-between;
|
|
254
|
+
align-items: center;
|
|
255
|
+
padding-bottom: 4px;
|
|
256
|
+
margin-bottom: 10px;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.fb-msg {
|
|
260
|
+
font-size: 14px;
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 8px;
|
|
264
|
+
padding: 12px;
|
|
265
|
+
margin-bottom: 8px;
|
|
266
|
+
border-radius: 6px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.fb-msg--header {
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
padding: 4px 8px;
|
|
272
|
+
margin-bottom: 0;
|
|
273
|
+
margin-left: auto;
|
|
274
|
+
margin-right: 8px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.fb-msg--ok {
|
|
278
|
+
background-color: #f0fdf4;
|
|
279
|
+
border: 1px solid #bbf7d0;
|
|
280
|
+
color: #16a34a;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.fb-msg--err {
|
|
284
|
+
background-color: #fef2f2;
|
|
285
|
+
border: 1px solid #fecaca;
|
|
286
|
+
color: #dc2626;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.fb-sub {
|
|
290
|
+
width: 100%;
|
|
291
|
+
padding: 16px 24px;
|
|
292
|
+
border: none;
|
|
293
|
+
border-radius: 12px;
|
|
294
|
+
font-size: 15px;
|
|
295
|
+
font-weight: 600;
|
|
296
|
+
cursor: pointer;
|
|
297
|
+
transition: all 200ms ease;
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
justify-content: center;
|
|
301
|
+
gap: 8px;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.fb-sub--dis {
|
|
305
|
+
cursor: not-allowed;
|
|
306
|
+
opacity: 0.5;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.fb-spin {
|
|
310
|
+
width: 16px;
|
|
311
|
+
height: 16px;
|
|
312
|
+
border: 2px solid transparent;
|
|
313
|
+
border-top: 2px solid currentColor;
|
|
314
|
+
border-radius: 50%;
|
|
315
|
+
animation: spin 1s linear infinite;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.fb-cnt {
|
|
319
|
+
padding: 24px;
|
|
320
|
+
overflow: auto;
|
|
321
|
+
max-height: calc(90vh - 48px);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.fb-ttl {
|
|
325
|
+
margin: 0;
|
|
326
|
+
font-size: 20px;
|
|
327
|
+
font-weight: 600;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.fb-ta {
|
|
331
|
+
width: 100%;
|
|
332
|
+
min-height: 100px;
|
|
333
|
+
padding: 12px;
|
|
334
|
+
border-radius: 8px;
|
|
335
|
+
font-size: 14px;
|
|
336
|
+
font-family: inherit;
|
|
337
|
+
outline: none;
|
|
338
|
+
resize: vertical;
|
|
339
|
+
transition: border-color 150ms ease;
|
|
340
|
+
margin-bottom: 4px;
|
|
341
|
+
box-sizing: border-box;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.fb-charcount {
|
|
345
|
+
font-size: 12px;
|
|
346
|
+
margin-left: auto;
|
|
347
|
+
margin-bottom: 8px;
|
|
348
|
+
text-align: right;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.fb-charcount--low {
|
|
352
|
+
color: #dc2626;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.fb-email {
|
|
356
|
+
display: flex;
|
|
357
|
+
flex-direction: column;
|
|
358
|
+
gap: 8px;
|
|
359
|
+
margin-bottom: 16px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.fb-email-lbl {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 8px;
|
|
366
|
+
font-size: 14px;
|
|
367
|
+
font-weight: 500;
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.fb-email-cb {
|
|
372
|
+
margin: 0;
|
|
373
|
+
cursor: pointer;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.fb-email-inp {
|
|
377
|
+
width: 100%;
|
|
378
|
+
padding: 8px 12px;
|
|
379
|
+
border-radius: 4px;
|
|
380
|
+
font-size: 14px;
|
|
381
|
+
outline: none;
|
|
382
|
+
transition: border-color 150ms ease;
|
|
383
|
+
box-sizing: border-box;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.fb-btn {
|
|
387
|
+
position: fixed;
|
|
388
|
+
width: 50px;
|
|
389
|
+
height: 50px;
|
|
390
|
+
border: none;
|
|
391
|
+
cursor: pointer;
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
justify-content: center;
|
|
395
|
+
font-size: 18px;
|
|
396
|
+
transition: all 300ms cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
|
397
|
+
z-index: 9998;
|
|
398
|
+
color: #ffffff;
|
|
399
|
+
top: 50%;
|
|
400
|
+
transform: translateY(-50%);
|
|
401
|
+
box-shadow: 0 4px 15px rgba(37, 99, 235, 0.3);
|
|
402
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
|
|
403
|
+
-webkit-font-smoothing: antialiased;
|
|
404
|
+
-moz-osx-font-smoothing: grayscale;
|
|
405
|
+
box-sizing: border-box;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.fb-btn--right {
|
|
409
|
+
right: -25px;
|
|
410
|
+
border-radius: 40px 0 0 40px;
|
|
411
|
+
padding-right: 8px;
|
|
412
|
+
box-shadow: -4px 0 15px rgba(37, 99, 235, 0.3);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.fb-btn--left {
|
|
416
|
+
left: -25px;
|
|
417
|
+
border-radius: 0 40px 40px 0;
|
|
418
|
+
padding-left: 8px;
|
|
419
|
+
box-shadow: 4px 0 15px rgba(37, 99, 235, 0.3);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.fb-btn--right.fb-btn--open {
|
|
423
|
+
right: -15px;
|
|
424
|
+
transform: translateY(-50%) scale(1.05);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.fb-btn--left.fb-btn--open {
|
|
428
|
+
left: -15px;
|
|
429
|
+
transform: translateY(-50%) scale(1.05);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.fb-backdrop {
|
|
433
|
+
position: fixed;
|
|
434
|
+
top: 0;
|
|
435
|
+
left: 0;
|
|
436
|
+
width: 100%;
|
|
437
|
+
height: 100%;
|
|
438
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
439
|
+
transition: opacity 300ms ease;
|
|
440
|
+
z-index: 9999;
|
|
441
|
+
backdrop-filter: blur(8px);
|
|
442
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
|
|
443
|
+
-webkit-font-smoothing: antialiased;
|
|
444
|
+
-moz-osx-font-smoothing: grayscale;
|
|
445
|
+
box-sizing: border-box;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.fb-pnl-base {
|
|
449
|
+
position: fixed;
|
|
450
|
+
top: 10vh;
|
|
451
|
+
width: 400px;
|
|
452
|
+
max-width: 90vw;
|
|
453
|
+
max-height: 60vh;
|
|
454
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
455
|
+
transition: transform 300ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
|
456
|
+
z-index: 10000;
|
|
457
|
+
display: flex;
|
|
458
|
+
flex-direction: column;
|
|
459
|
+
overflow-y: auto;
|
|
460
|
+
overflow-x: hidden;
|
|
461
|
+
border-radius: 16px;
|
|
462
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif;
|
|
463
|
+
-webkit-font-smoothing: antialiased;
|
|
464
|
+
-moz-osx-font-smoothing: grayscale;
|
|
465
|
+
box-sizing: border-box;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.fb-pnl--right { right: 0; }
|
|
469
|
+
.fb-pnl--right.fb-pnl--open { transform: translateX(0px); }
|
|
470
|
+
.fb-pnl--right.fb-pnl--closed { transform: translateX(100%); }
|
|
471
|
+
|
|
472
|
+
.fb-pnl--left { left: 0; }
|
|
473
|
+
.fb-pnl--left.fb-pnl--open { transform: translateX(0px); }
|
|
474
|
+
.fb-pnl--left.fb-pnl--closed { transform: translateX(-100%); }
|
|
475
|
+
|
|
476
|
+
.fb-close-btn {
|
|
477
|
+
background: none;
|
|
478
|
+
border: none;
|
|
479
|
+
font-size: 24px;
|
|
480
|
+
cursor: pointer;
|
|
481
|
+
opacity: 0.7;
|
|
482
|
+
padding: 0;
|
|
483
|
+
width: 32px;
|
|
484
|
+
height: 32px;
|
|
485
|
+
display: flex;
|
|
486
|
+
align-items: center;
|
|
487
|
+
justify-content: center;
|
|
488
|
+
border-radius: 4px;
|
|
489
|
+
transition: background-color 150ms ease;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.fb-up {
|
|
493
|
+
display: flex;
|
|
494
|
+
flex-direction: column;
|
|
495
|
+
gap: 8px;
|
|
496
|
+
margin-bottom: 12px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.fb-upb {
|
|
500
|
+
display: inline-flex;
|
|
501
|
+
align-items: center;
|
|
502
|
+
gap: 6px;
|
|
503
|
+
align-self: flex-start;
|
|
504
|
+
padding: 8px 12px;
|
|
505
|
+
border-radius: 8px;
|
|
506
|
+
background: transparent;
|
|
507
|
+
font-size: 13px;
|
|
508
|
+
font-weight: 500;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
transition: background-color 150ms ease, opacity 150ms ease;
|
|
511
|
+
font-family: inherit;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.fb-upb:hover:not(.fb-upb--dis) {
|
|
515
|
+
background-color: rgba(37, 99, 235, 0.06);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.fb-upb--dis {
|
|
519
|
+
cursor: not-allowed;
|
|
520
|
+
opacity: 0.5;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.fb-ups {
|
|
524
|
+
width: 12px;
|
|
525
|
+
height: 12px;
|
|
526
|
+
border: 2px solid transparent;
|
|
527
|
+
border-top: 2px solid currentColor;
|
|
528
|
+
border-radius: 50%;
|
|
529
|
+
animation: spin 1s linear infinite;
|
|
530
|
+
display: inline-block;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.fb-up-extras {
|
|
534
|
+
display: flex;
|
|
535
|
+
flex-direction: column;
|
|
536
|
+
gap: 6px;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.fb-upe {
|
|
540
|
+
font-size: 12px;
|
|
541
|
+
color: #dc2626;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.fb-ss {
|
|
545
|
+
display: flex;
|
|
546
|
+
flex-wrap: wrap;
|
|
547
|
+
gap: 8px;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.fb-sp {
|
|
551
|
+
position: relative;
|
|
552
|
+
width: 64px;
|
|
553
|
+
height: 64px;
|
|
554
|
+
border-radius: 6px;
|
|
555
|
+
overflow: hidden;
|
|
556
|
+
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.fb-si {
|
|
560
|
+
width: 100%;
|
|
561
|
+
height: 100%;
|
|
562
|
+
object-fit: cover;
|
|
563
|
+
display: block;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.fb-sr {
|
|
567
|
+
position: absolute;
|
|
568
|
+
top: 2px;
|
|
569
|
+
right: 2px;
|
|
570
|
+
width: 18px;
|
|
571
|
+
height: 18px;
|
|
572
|
+
border-radius: 50%;
|
|
573
|
+
border: none;
|
|
574
|
+
background: rgba(0, 0, 0, 0.65);
|
|
575
|
+
color: #fff;
|
|
576
|
+
font-size: 11px;
|
|
577
|
+
line-height: 1;
|
|
578
|
+
cursor: pointer;
|
|
579
|
+
display: flex;
|
|
580
|
+
align-items: center;
|
|
581
|
+
justify-content: center;
|
|
582
|
+
padding: 0;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.fb-sr:hover {
|
|
586
|
+
background: rgba(0, 0, 0, 0.85);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.fb-sl {
|
|
590
|
+
font-size: 11px;
|
|
591
|
+
opacity: 0.6;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@media (max-width: 768px) {
|
|
595
|
+
.fb-pnl-base {
|
|
596
|
+
width: 100% !important;
|
|
597
|
+
max-width: none !important;
|
|
598
|
+
top: 5vh !important;
|
|
599
|
+
max-height: 70vh !important;
|
|
600
|
+
}
|
|
601
|
+
.fb-cnt { padding: 20px !important; max-height: calc(100vh - 80px) !important; }
|
|
602
|
+
.fb-ta { font-size: 16px !important; min-height: 80px !important; }
|
|
603
|
+
.fb-ttl { font-size: 18px !important; }
|
|
604
|
+
.fb-ei { font-size: 24px !important; }
|
|
605
|
+
.fb-el { font-size: 11px !important; }
|
|
606
|
+
.fb-sub { padding: 14px 20px !important; font-size: 16px !important; }
|
|
607
|
+
}
|
|
608
|
+
`;
|
|
609
|
+
|
|
610
|
+
// src/vanilla.ts
|
|
611
|
+
var EMAIL_STORAGE_KEY = "feedback_user_email";
|
|
612
|
+
function escapeHtml(value) {
|
|
613
|
+
return value.replace(/[&<>"']/g, (ch) => {
|
|
614
|
+
switch (ch) {
|
|
615
|
+
case "&":
|
|
616
|
+
return "&";
|
|
617
|
+
case "<":
|
|
618
|
+
return "<";
|
|
619
|
+
case ">":
|
|
620
|
+
return ">";
|
|
621
|
+
case '"':
|
|
622
|
+
return """;
|
|
623
|
+
case "'":
|
|
624
|
+
return "'";
|
|
625
|
+
default:
|
|
626
|
+
return ch;
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function readStoredEmail() {
|
|
631
|
+
if (typeof window === "undefined") return "";
|
|
632
|
+
try {
|
|
633
|
+
return window.localStorage.getItem(EMAIL_STORAGE_KEY) ?? "";
|
|
634
|
+
} catch {
|
|
635
|
+
return "";
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function writeStoredEmail(email) {
|
|
639
|
+
try {
|
|
640
|
+
window.localStorage.setItem(EMAIL_STORAGE_KEY, email);
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function initUseroFeedbackWidget(props) {
|
|
645
|
+
if (typeof document === "undefined") {
|
|
646
|
+
return {
|
|
647
|
+
destroy: () => {
|
|
648
|
+
},
|
|
649
|
+
open: () => {
|
|
650
|
+
},
|
|
651
|
+
close: () => {
|
|
652
|
+
},
|
|
653
|
+
update: () => {
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
const { clientId, baseUrl } = props;
|
|
658
|
+
if (!clientId || clientId.length < 3) {
|
|
659
|
+
const err = new Error("Invalid config. Contact admin.");
|
|
660
|
+
props.onError?.(err);
|
|
661
|
+
return {
|
|
662
|
+
destroy: () => {
|
|
663
|
+
},
|
|
664
|
+
open: () => {
|
|
665
|
+
},
|
|
666
|
+
close: () => {
|
|
667
|
+
},
|
|
668
|
+
update: () => {
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
let position = props.position ?? "right";
|
|
673
|
+
let theme = mergeTheme(props.theme);
|
|
674
|
+
let title = props.title ?? "Share Feedback";
|
|
675
|
+
let placeholder = props.placeholder ?? "Tell us what you think... (optional)";
|
|
676
|
+
let showEmailOption = props.showEmailOption ?? true;
|
|
677
|
+
let showScreenshotOption = props.showScreenshotOption ?? true;
|
|
678
|
+
let environment = props.environment;
|
|
679
|
+
let metadata = props.metadata;
|
|
680
|
+
let onSubmit = props.onSubmit;
|
|
681
|
+
let onError = props.onError;
|
|
682
|
+
let onOpen = props.onOpen;
|
|
683
|
+
let onClose = props.onClose;
|
|
684
|
+
const apiClient = new FeedbackApiClient(baseUrl);
|
|
685
|
+
let isOpen = false;
|
|
686
|
+
let selectedRating = void 0;
|
|
687
|
+
let comment = "";
|
|
688
|
+
let shareEmail = false;
|
|
689
|
+
let userEmail = readStoredEmail();
|
|
690
|
+
let isSubmitting = false;
|
|
691
|
+
let submitMessage = null;
|
|
692
|
+
let screenshots = [];
|
|
693
|
+
let isUploadingScreenshot = false;
|
|
694
|
+
let screenshotError = null;
|
|
695
|
+
const MAX_SCREENSHOTS = 3;
|
|
696
|
+
const MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
|
|
697
|
+
const host = document.createElement("div");
|
|
698
|
+
host.setAttribute("data-usero-widget", "");
|
|
699
|
+
host.style.cssText = "all: initial;";
|
|
700
|
+
document.body.appendChild(host);
|
|
701
|
+
const root = host.attachShadow({ mode: "open" });
|
|
702
|
+
const style = document.createElement("style");
|
|
703
|
+
style.textContent = FEEDBACK_CSS;
|
|
704
|
+
root.appendChild(style);
|
|
705
|
+
const buttonEl = document.createElement("button");
|
|
706
|
+
const backdropEl = document.createElement("div");
|
|
707
|
+
const panelEl = document.createElement("div");
|
|
708
|
+
root.appendChild(buttonEl);
|
|
709
|
+
root.appendChild(backdropEl);
|
|
710
|
+
root.appendChild(panelEl);
|
|
711
|
+
function setSubmitMessage(next) {
|
|
712
|
+
submitMessage = next;
|
|
713
|
+
render();
|
|
714
|
+
}
|
|
715
|
+
function open() {
|
|
716
|
+
if (isOpen) return;
|
|
717
|
+
isOpen = true;
|
|
718
|
+
selectedRating = void 0;
|
|
719
|
+
comment = "";
|
|
720
|
+
shareEmail = false;
|
|
721
|
+
submitMessage = null;
|
|
722
|
+
screenshots = [];
|
|
723
|
+
screenshotError = null;
|
|
724
|
+
isUploadingScreenshot = false;
|
|
725
|
+
apiClient.ping();
|
|
726
|
+
onOpen?.();
|
|
727
|
+
render();
|
|
728
|
+
}
|
|
729
|
+
async function handleScreenshotFile(file) {
|
|
730
|
+
screenshotError = null;
|
|
731
|
+
if (!file.type.startsWith("image/")) {
|
|
732
|
+
screenshotError = "Image files only";
|
|
733
|
+
render();
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (file.size > MAX_SCREENSHOT_BYTES) {
|
|
737
|
+
screenshotError = "Max 10MB";
|
|
738
|
+
render();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (screenshots.length >= MAX_SCREENSHOTS) {
|
|
742
|
+
screenshotError = `Max ${MAX_SCREENSHOTS} screenshots`;
|
|
743
|
+
render();
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
isUploadingScreenshot = true;
|
|
747
|
+
render();
|
|
748
|
+
try {
|
|
749
|
+
const uploaded = await apiClient.uploadScreenshot(file, clientId);
|
|
750
|
+
screenshots = [...screenshots, uploaded];
|
|
751
|
+
} catch (err) {
|
|
752
|
+
screenshotError = err instanceof Error ? err.message : "Upload failed";
|
|
753
|
+
} finally {
|
|
754
|
+
isUploadingScreenshot = false;
|
|
755
|
+
render();
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function removeScreenshot(index) {
|
|
759
|
+
screenshots = screenshots.filter((_, i) => i !== index);
|
|
760
|
+
render();
|
|
761
|
+
}
|
|
762
|
+
function close() {
|
|
763
|
+
if (!isOpen) return;
|
|
764
|
+
isOpen = false;
|
|
765
|
+
onClose?.();
|
|
766
|
+
render();
|
|
767
|
+
}
|
|
768
|
+
async function submitForm() {
|
|
769
|
+
if (isSubmitting) return;
|
|
770
|
+
isSubmitting = true;
|
|
771
|
+
submitMessage = null;
|
|
772
|
+
render();
|
|
773
|
+
const feedbackData = {
|
|
774
|
+
rating: selectedRating,
|
|
775
|
+
comment: comment.trim() || void 0,
|
|
776
|
+
userEmail: shareEmail ? userEmail : void 0,
|
|
777
|
+
screenshots: screenshots.length > 0 ? screenshots : void 0,
|
|
778
|
+
metadata: {
|
|
779
|
+
pageUrl: window.location.href,
|
|
780
|
+
pageTitle: document.title || "Untitled Page",
|
|
781
|
+
referrer: document.referrer || void 0,
|
|
782
|
+
timestamp: Date.now()
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
const submission = {
|
|
786
|
+
clientId,
|
|
787
|
+
rating: feedbackData.rating,
|
|
788
|
+
comment: feedbackData.comment,
|
|
789
|
+
userEmail: feedbackData.userEmail,
|
|
790
|
+
pageUrl: feedbackData.metadata.pageUrl,
|
|
791
|
+
pageTitle: feedbackData.metadata.pageTitle,
|
|
792
|
+
referrer: feedbackData.metadata.referrer,
|
|
793
|
+
environment
|
|
794
|
+
};
|
|
795
|
+
if (screenshots.length > 0) submission.screenshots = screenshots;
|
|
796
|
+
if (metadata !== void 0) submission.metadata = metadata;
|
|
797
|
+
const validation = validateFeedbackSubmission(submission);
|
|
798
|
+
if (!validation.isValid) {
|
|
799
|
+
isSubmitting = false;
|
|
800
|
+
setSubmitMessage({ type: "error", text: validation.errors.join(", ") });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
try {
|
|
804
|
+
const response = await apiClient.submitFeedback(submission);
|
|
805
|
+
if (response.success) {
|
|
806
|
+
if (shareEmail && userEmail) writeStoredEmail(userEmail);
|
|
807
|
+
onSubmit?.(feedbackData);
|
|
808
|
+
selectedRating = void 0;
|
|
809
|
+
comment = "";
|
|
810
|
+
shareEmail = false;
|
|
811
|
+
screenshots = [];
|
|
812
|
+
screenshotError = null;
|
|
813
|
+
submitMessage = { type: "success", text: "Thank you!" };
|
|
814
|
+
} else {
|
|
815
|
+
const msg = response.error ?? "Error occurred. Try again.";
|
|
816
|
+
onError?.(new Error(msg));
|
|
817
|
+
submitMessage = { type: "error", text: msg };
|
|
818
|
+
}
|
|
819
|
+
} catch (err) {
|
|
820
|
+
const msg = err instanceof Error ? err.message : "Error occurred. Try again.";
|
|
821
|
+
onError?.(new Error(msg));
|
|
822
|
+
submitMessage = { type: "error", text: msg };
|
|
823
|
+
} finally {
|
|
824
|
+
isSubmitting = false;
|
|
825
|
+
render();
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function renderButton() {
|
|
829
|
+
buttonEl.className = `fb-btn fb-btn--${position} ${isOpen ? "fb-btn--open" : ""}`;
|
|
830
|
+
buttonEl.setAttribute("aria-label", "Open feedback");
|
|
831
|
+
buttonEl.type = "button";
|
|
832
|
+
buttonEl.style.background = `linear-gradient(135deg, ${theme.primary}, ${getGradientEnd(theme.primary)})`;
|
|
833
|
+
buttonEl.innerHTML = isOpen ? `<span style="font-size:20px;">\u2715</span>` : "";
|
|
834
|
+
}
|
|
835
|
+
function renderBackdrop() {
|
|
836
|
+
backdropEl.className = "fb-backdrop";
|
|
837
|
+
backdropEl.style.display = isOpen ? "block" : "none";
|
|
838
|
+
backdropEl.setAttribute("aria-label", "Close modal");
|
|
839
|
+
}
|
|
840
|
+
function renderPanel() {
|
|
841
|
+
panelEl.className = `fb-pnl-base fb-pnl--${position} ${isOpen ? "fb-pnl--open" : "fb-pnl--closed"}`;
|
|
842
|
+
panelEl.style.backgroundColor = theme.background;
|
|
843
|
+
if (position === "right") {
|
|
844
|
+
panelEl.style.borderLeft = `1px solid ${theme.border}`;
|
|
845
|
+
panelEl.style.borderRight = "";
|
|
846
|
+
} else {
|
|
847
|
+
panelEl.style.borderRight = `1px solid ${theme.border}`;
|
|
848
|
+
panelEl.style.borderLeft = "";
|
|
849
|
+
}
|
|
850
|
+
panelEl.setAttribute("role", "dialog");
|
|
851
|
+
panelEl.setAttribute("aria-modal", "true");
|
|
852
|
+
panelEl.setAttribute("aria-labelledby", "usero-feedback-title");
|
|
853
|
+
const remaining = 1e3 - comment.length;
|
|
854
|
+
const lowChars = remaining < 50;
|
|
855
|
+
const ratingsHtml = [1, 2, 3, 4].map((r) => {
|
|
856
|
+
const sel = selectedRating === r;
|
|
857
|
+
const bg = EMOJI_BACKGROUNDS[r];
|
|
858
|
+
const cls = ["fb-ec", sel && "fb-ec--sel"].filter(Boolean).join(" ");
|
|
859
|
+
return `
|
|
860
|
+
<div class="${cls}" style="background:${bg}">
|
|
861
|
+
<button type="button" class="fb-eb" data-rating="${r}" role="radio" aria-checked="${sel}" aria-label="${r}: ${RATING_LABELS[r]}">
|
|
862
|
+
<div class="fb-ei"><span role="img" aria-label="${RATING_LABELS[r]}">${EMOJI_MAP[r]}</span></div>
|
|
863
|
+
<div class="fb-el">${RATING_LABELS[r]}</div>
|
|
864
|
+
</button>
|
|
865
|
+
</div>
|
|
866
|
+
`;
|
|
867
|
+
}).join("");
|
|
868
|
+
const messageHtml = submitMessage ? `<div class="fb-msg fb-msg--header ${submitMessage.type === "success" ? "fb-msg--ok" : "fb-msg--err"}">${submitMessage.type === "success" ? "\u2713" : "\u26A0"} ${escapeHtml(submitMessage.text)}</div>` : "";
|
|
869
|
+
const screenshotBlockHtml = showScreenshotOption ? (() => {
|
|
870
|
+
const atMax = screenshots.length >= MAX_SCREENSHOTS;
|
|
871
|
+
const btnDisabled = isUploadingScreenshot || atMax;
|
|
872
|
+
const previewsHtml = screenshots.map(
|
|
873
|
+
(shot, i) => `
|
|
874
|
+
<div class="fb-sp">
|
|
875
|
+
<img src="${escapeHtml(shot.url)}" alt="Screenshot ${i + 1}" class="fb-si" />
|
|
876
|
+
<button type="button" class="fb-sr" data-role="screenshot-remove" data-index="${i}" aria-label="Remove screenshot">\u2715</button>
|
|
877
|
+
</div>
|
|
878
|
+
`
|
|
879
|
+
).join("");
|
|
880
|
+
const errorHtml = screenshotError ? `<div class="fb-upe">\u26A0 ${escapeHtml(screenshotError)}</div>` : "";
|
|
881
|
+
const limitHtml = atMax ? `<div class="fb-sl">Max ${MAX_SCREENSHOTS}</div>` : "";
|
|
882
|
+
const extrasHtml = screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
|
|
883
|
+
return `
|
|
884
|
+
<div class="fb-up">
|
|
885
|
+
<input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
|
|
886
|
+
<button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
|
|
887
|
+
${isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot"}
|
|
888
|
+
</button>
|
|
889
|
+
${extrasHtml}
|
|
890
|
+
</div>
|
|
891
|
+
`;
|
|
892
|
+
})() : "";
|
|
893
|
+
const emailBlockHtml = showEmailOption ? `
|
|
894
|
+
<div class="fb-email">
|
|
895
|
+
<label class="fb-email-lbl" style="color:${theme.text}">
|
|
896
|
+
<input type="checkbox" class="fb-email-cb" data-role="share-email" ${shareEmail ? "checked" : ""} aria-label="Share email" />
|
|
897
|
+
<span>Share my email</span>
|
|
898
|
+
</label>
|
|
899
|
+
${shareEmail ? `<input type="email" class="fb-email-inp" data-role="email-input" value="${escapeHtml(userEmail)}" placeholder="your.email@example.com" aria-label="Email" maxlength="254" autocomplete="email" style="border:1px solid ${theme.border};color:${theme.text};background-color:${theme.background};" />` : ""}
|
|
900
|
+
</div>
|
|
901
|
+
` : "";
|
|
902
|
+
const submitDisabled = isSubmitting;
|
|
903
|
+
const submitStyle = `background:linear-gradient(135deg, ${theme.primary}, ${getGradientEnd(theme.primary)});color:#ffffff;${submitDisabled ? "opacity:0.6;cursor:not-allowed;" : ""}`;
|
|
904
|
+
panelEl.innerHTML = `
|
|
905
|
+
<div class="fb-cnt">
|
|
906
|
+
<div class="fb-hdr" style="border-bottom:1px solid ${theme.border}">
|
|
907
|
+
<h2 id="usero-feedback-title" class="fb-ttl" style="color:${theme.text}">${escapeHtml(title)}</h2>
|
|
908
|
+
${messageHtml}
|
|
909
|
+
<button class="fb-close-btn" data-role="close" style="color:${theme.text}" aria-label="Close" type="button">\u2715</button>
|
|
910
|
+
</div>
|
|
911
|
+
<form data-role="form">
|
|
912
|
+
<div class="fb-es" role="radiogroup" aria-label="Rate experience">${ratingsHtml}</div>
|
|
913
|
+
<textarea class="fb-ta" data-role="comment" placeholder="${escapeHtml(placeholder)}" aria-label="Comments" maxlength="1000" rows="2" style="border:1px solid ${theme.border};color:${theme.text};background-color:${theme.background};">${escapeHtml(comment)}</textarea>
|
|
914
|
+
<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">
|
|
915
|
+
<div style="font-size:12px;color:${lowChars ? "#dc2626" : theme.text};opacity:${lowChars ? 1 : 0.6};margin-left:auto;">
|
|
916
|
+
${remaining} chars remaining
|
|
917
|
+
</div>
|
|
918
|
+
</div>
|
|
919
|
+
${screenshotBlockHtml}
|
|
920
|
+
${emailBlockHtml}
|
|
921
|
+
<button class="fb-sub ${submitDisabled ? "fb-sub--dis" : ""}" type="submit" aria-label="Submit" ${submitDisabled ? "disabled" : ""} style="${submitStyle}">
|
|
922
|
+
${isSubmitting ? '<span class="fb-spin"></span>' : ""}
|
|
923
|
+
${isSubmitting ? "Submitting..." : "Send Feedback \u{1F680}"}
|
|
924
|
+
</button>
|
|
925
|
+
</form>
|
|
926
|
+
</div>
|
|
927
|
+
`;
|
|
928
|
+
const form = panelEl.querySelector('form[data-role="form"]');
|
|
929
|
+
form?.addEventListener("submit", (e) => {
|
|
930
|
+
e.preventDefault();
|
|
931
|
+
void submitForm();
|
|
932
|
+
});
|
|
933
|
+
panelEl.querySelector('button[data-role="close"]')?.addEventListener("click", close);
|
|
934
|
+
panelEl.querySelectorAll("button[data-rating]").forEach((btn) => {
|
|
935
|
+
btn.addEventListener("click", () => {
|
|
936
|
+
const value = btn.dataset.rating;
|
|
937
|
+
if (value === "1" || value === "2" || value === "3" || value === "4") {
|
|
938
|
+
selectedRating = Number(value);
|
|
939
|
+
render();
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
const textarea = panelEl.querySelector(
|
|
944
|
+
'textarea[data-role="comment"]'
|
|
945
|
+
);
|
|
946
|
+
if (textarea) {
|
|
947
|
+
textarea.addEventListener("input", () => {
|
|
948
|
+
if (textarea.value.length <= 1e3) {
|
|
949
|
+
comment = textarea.value;
|
|
950
|
+
const counter = panelEl.querySelector(
|
|
951
|
+
".fb-cnt form > div > div"
|
|
952
|
+
);
|
|
953
|
+
if (counter) {
|
|
954
|
+
const left = 1e3 - comment.length;
|
|
955
|
+
counter.textContent = `${left} chars remaining`;
|
|
956
|
+
counter.style.color = left < 50 ? "#dc2626" : theme.text;
|
|
957
|
+
counter.style.opacity = left < 50 ? "1" : "0.6";
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
const shareCb = panelEl.querySelector(
|
|
963
|
+
'input[data-role="share-email"]'
|
|
964
|
+
);
|
|
965
|
+
shareCb?.addEventListener("change", () => {
|
|
966
|
+
shareEmail = shareCb.checked;
|
|
967
|
+
render();
|
|
968
|
+
});
|
|
969
|
+
const emailInp = panelEl.querySelector(
|
|
970
|
+
'input[data-role="email-input"]'
|
|
971
|
+
);
|
|
972
|
+
emailInp?.addEventListener("input", () => {
|
|
973
|
+
if (emailInp.value.length <= 254) {
|
|
974
|
+
userEmail = emailInp.value;
|
|
975
|
+
}
|
|
976
|
+
});
|
|
977
|
+
const fileInput = panelEl.querySelector(
|
|
978
|
+
'input[data-role="screenshot-input"]'
|
|
979
|
+
);
|
|
980
|
+
const pickBtn = panelEl.querySelector(
|
|
981
|
+
'button[data-role="screenshot-pick"]'
|
|
982
|
+
);
|
|
983
|
+
pickBtn?.addEventListener("click", () => {
|
|
984
|
+
fileInput?.click();
|
|
985
|
+
});
|
|
986
|
+
fileInput?.addEventListener("change", () => {
|
|
987
|
+
const file = fileInput.files?.[0];
|
|
988
|
+
if (!file) return;
|
|
989
|
+
void handleScreenshotFile(file).finally(() => {
|
|
990
|
+
if (fileInput) fileInput.value = "";
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
panelEl.querySelectorAll(
|
|
994
|
+
'button[data-role="screenshot-remove"]'
|
|
995
|
+
).forEach((btn) => {
|
|
996
|
+
btn.addEventListener("click", () => {
|
|
997
|
+
const idx = Number(btn.dataset.index);
|
|
998
|
+
if (Number.isInteger(idx)) removeScreenshot(idx);
|
|
999
|
+
});
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
function render() {
|
|
1003
|
+
renderButton();
|
|
1004
|
+
renderBackdrop();
|
|
1005
|
+
renderPanel();
|
|
1006
|
+
}
|
|
1007
|
+
buttonEl.addEventListener("click", () => {
|
|
1008
|
+
if (isOpen) close();
|
|
1009
|
+
else open();
|
|
1010
|
+
});
|
|
1011
|
+
backdropEl.addEventListener("click", close);
|
|
1012
|
+
const onKeyDown = (e) => {
|
|
1013
|
+
if (!isOpen) return;
|
|
1014
|
+
if (e.key === "Escape") close();
|
|
1015
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
1016
|
+
e.preventDefault();
|
|
1017
|
+
void submitForm();
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1021
|
+
render();
|
|
1022
|
+
let destroyed = false;
|
|
1023
|
+
return {
|
|
1024
|
+
destroy: () => {
|
|
1025
|
+
if (destroyed) return;
|
|
1026
|
+
destroyed = true;
|
|
1027
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1028
|
+
host.remove();
|
|
1029
|
+
},
|
|
1030
|
+
open,
|
|
1031
|
+
close,
|
|
1032
|
+
update: (next) => {
|
|
1033
|
+
if (destroyed) return;
|
|
1034
|
+
let needsRender = false;
|
|
1035
|
+
if (next.position !== void 0 && next.position !== position) {
|
|
1036
|
+
position = next.position;
|
|
1037
|
+
needsRender = true;
|
|
1038
|
+
}
|
|
1039
|
+
if (next.theme !== void 0) {
|
|
1040
|
+
theme = mergeTheme(next.theme);
|
|
1041
|
+
needsRender = true;
|
|
1042
|
+
}
|
|
1043
|
+
if (next.title !== void 0 && next.title !== title) {
|
|
1044
|
+
title = next.title;
|
|
1045
|
+
needsRender = true;
|
|
1046
|
+
}
|
|
1047
|
+
if (next.placeholder !== void 0 && next.placeholder !== placeholder) {
|
|
1048
|
+
placeholder = next.placeholder;
|
|
1049
|
+
needsRender = true;
|
|
1050
|
+
}
|
|
1051
|
+
if (next.showEmailOption !== void 0 && next.showEmailOption !== showEmailOption) {
|
|
1052
|
+
showEmailOption = next.showEmailOption;
|
|
1053
|
+
needsRender = true;
|
|
1054
|
+
}
|
|
1055
|
+
if (next.showScreenshotOption !== void 0 && next.showScreenshotOption !== showScreenshotOption) {
|
|
1056
|
+
showScreenshotOption = next.showScreenshotOption;
|
|
1057
|
+
needsRender = true;
|
|
1058
|
+
}
|
|
1059
|
+
if ("environment" in next) environment = next.environment;
|
|
1060
|
+
if ("metadata" in next) metadata = next.metadata;
|
|
1061
|
+
if ("onSubmit" in next) onSubmit = next.onSubmit;
|
|
1062
|
+
if ("onError" in next) onError = next.onError;
|
|
1063
|
+
if ("onOpen" in next) onOpen = next.onOpen;
|
|
1064
|
+
if ("onClose" in next) onClose = next.onClose;
|
|
1065
|
+
if (needsRender) render();
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// src/react.tsx
|
|
1071
|
+
function UseroFeedbackWidget(props) {
|
|
1072
|
+
const handleRef = react.useRef(null);
|
|
1073
|
+
const callbacksRef = react.useRef({
|
|
1074
|
+
onSubmit: props.onSubmit,
|
|
1075
|
+
onError: props.onError,
|
|
1076
|
+
onOpen: props.onOpen,
|
|
1077
|
+
onClose: props.onClose
|
|
1078
|
+
});
|
|
1079
|
+
callbacksRef.current = {
|
|
1080
|
+
onSubmit: props.onSubmit,
|
|
1081
|
+
onError: props.onError,
|
|
1082
|
+
onOpen: props.onOpen,
|
|
1083
|
+
onClose: props.onClose
|
|
1084
|
+
};
|
|
1085
|
+
const { clientId, baseUrl } = props;
|
|
1086
|
+
react.useEffect(() => {
|
|
1087
|
+
const handle = initUseroFeedbackWidget({
|
|
1088
|
+
...props,
|
|
1089
|
+
onSubmit: (data) => callbacksRef.current.onSubmit?.(data),
|
|
1090
|
+
onError: (err) => callbacksRef.current.onError?.(err),
|
|
1091
|
+
onOpen: () => callbacksRef.current.onOpen?.(),
|
|
1092
|
+
onClose: () => callbacksRef.current.onClose?.()
|
|
1093
|
+
});
|
|
1094
|
+
handleRef.current = handle;
|
|
1095
|
+
return () => {
|
|
1096
|
+
handle.destroy();
|
|
1097
|
+
handleRef.current = null;
|
|
1098
|
+
};
|
|
1099
|
+
}, [clientId, baseUrl]);
|
|
1100
|
+
const themeJson = JSON.stringify(props.theme ?? null);
|
|
1101
|
+
const metadataJson = JSON.stringify(props.metadata ?? null);
|
|
1102
|
+
react.useEffect(() => {
|
|
1103
|
+
const handle = handleRef.current;
|
|
1104
|
+
if (!handle) return;
|
|
1105
|
+
const updates = {};
|
|
1106
|
+
if (props.position !== void 0) updates.position = props.position;
|
|
1107
|
+
if (props.theme !== void 0) updates.theme = props.theme;
|
|
1108
|
+
if (props.title !== void 0) updates.title = props.title;
|
|
1109
|
+
if (props.placeholder !== void 0) updates.placeholder = props.placeholder;
|
|
1110
|
+
if (props.showEmailOption !== void 0) {
|
|
1111
|
+
updates.showEmailOption = props.showEmailOption;
|
|
1112
|
+
}
|
|
1113
|
+
if (props.showScreenshotOption !== void 0) {
|
|
1114
|
+
updates.showScreenshotOption = props.showScreenshotOption;
|
|
1115
|
+
}
|
|
1116
|
+
if (props.environment !== void 0) updates.environment = props.environment;
|
|
1117
|
+
if (props.metadata !== void 0) updates.metadata = props.metadata;
|
|
1118
|
+
handle.update(updates);
|
|
1119
|
+
}, [
|
|
1120
|
+
props.position,
|
|
1121
|
+
themeJson,
|
|
1122
|
+
props.title,
|
|
1123
|
+
props.placeholder,
|
|
1124
|
+
props.showEmailOption,
|
|
1125
|
+
props.showScreenshotOption,
|
|
1126
|
+
props.environment,
|
|
1127
|
+
metadataJson
|
|
1128
|
+
]);
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
exports.DARK_THEME = DARK_THEME;
|
|
1133
|
+
exports.DEFAULT_THEME = DEFAULT_THEME;
|
|
1134
|
+
exports.UseroFeedbackWidget = UseroFeedbackWidget;
|
|
1135
|
+
exports.mergeTheme = mergeTheme;
|
|
1136
|
+
//# sourceMappingURL=react.cjs.map
|
|
1137
|
+
//# sourceMappingURL=react.cjs.map
|