@product7/feedback-sdk 1.0.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/README.md +227 -0
- package/dist/README.md +227 -0
- package/dist/feedback-sdk.js +2483 -0
- package/dist/feedback-sdk.js.map +1 -0
- package/dist/feedback-sdk.min.js +2 -0
- package/dist/feedback-sdk.min.js.map +1 -0
- package/package.json +111 -0
- package/src/core/APIService.js +338 -0
- package/src/core/EventBus.js +54 -0
- package/src/core/FeedbackSDK.js +285 -0
- package/src/docs/api.md +686 -0
- package/src/docs/example.md +823 -0
- package/src/docs/installation.md +264 -0
- package/src/index.js +281 -0
- package/src/styles/styles.js +557 -0
- package/src/types/index.d.ts +12 -0
- package/src/utils/errors.js +142 -0
- package/src/utils/helpers.js +219 -0
- package/src/widgets/BaseWidget.js +334 -0
- package/src/widgets/ButtonWidget.js +62 -0
- package/src/widgets/InlineWidget.js +145 -0
- package/src/widgets/TabWidget.js +65 -0
- package/src/widgets/WidgetFactory.js +64 -0
|
@@ -0,0 +1,2483 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
3
|
+
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
4
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FeedbackSDK = {}));
|
|
5
|
+
})(this, (function (exports) { 'use strict';
|
|
6
|
+
|
|
7
|
+
class SDKError extends Error {
|
|
8
|
+
constructor(message, cause) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'SDKError';
|
|
11
|
+
this.cause = cause;
|
|
12
|
+
|
|
13
|
+
if (Error.captureStackTrace) {
|
|
14
|
+
Error.captureStackTrace(this, SDKError);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class APIError extends Error {
|
|
20
|
+
constructor(status, message, response) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'APIError';
|
|
23
|
+
this.status = status;
|
|
24
|
+
this.response = response;
|
|
25
|
+
|
|
26
|
+
if (Error.captureStackTrace) {
|
|
27
|
+
Error.captureStackTrace(this, APIError);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
isNetworkError() {
|
|
32
|
+
return this.status === 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
isClientError() {
|
|
36
|
+
return this.status >= 400 && this.status < 500;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
isServerError() {
|
|
40
|
+
return this.status >= 500 && this.status < 600;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class WidgetError extends Error {
|
|
45
|
+
constructor(message, widgetType, widgetId) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'WidgetError';
|
|
48
|
+
this.widgetType = widgetType;
|
|
49
|
+
this.widgetId = widgetId;
|
|
50
|
+
|
|
51
|
+
if (Error.captureStackTrace) {
|
|
52
|
+
Error.captureStackTrace(this, WidgetError);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class ConfigError extends Error {
|
|
58
|
+
constructor(message, configKey) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = 'ConfigError';
|
|
61
|
+
this.configKey = configKey;
|
|
62
|
+
|
|
63
|
+
if (Error.captureStackTrace) {
|
|
64
|
+
Error.captureStackTrace(this, ConfigError);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ValidationError extends Error {
|
|
70
|
+
constructor(message, field, value) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.name = 'ValidationError';
|
|
73
|
+
this.field = field;
|
|
74
|
+
this.value = value;
|
|
75
|
+
|
|
76
|
+
if (Error.captureStackTrace) {
|
|
77
|
+
Error.captureStackTrace(this, ValidationError);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class APIService {
|
|
83
|
+
constructor(config = {}) {
|
|
84
|
+
this.workspace = config.workspace;
|
|
85
|
+
this.sessionToken = null;
|
|
86
|
+
this.sessionExpiry = null;
|
|
87
|
+
this.userContext = config.userContext || null;
|
|
88
|
+
|
|
89
|
+
// Construct workspace-specific API URL
|
|
90
|
+
if (config.apiUrl) {
|
|
91
|
+
// If explicitly provided, use it
|
|
92
|
+
this.baseURL = config.apiUrl;
|
|
93
|
+
} else if (this.workspace) {
|
|
94
|
+
// Construct from workspace: workspace.api.staging.product7.io/api/v1
|
|
95
|
+
this.baseURL = `https://${this.workspace}.api.staging.product7.io/api/v1`;
|
|
96
|
+
} else {
|
|
97
|
+
// Fallback to default
|
|
98
|
+
this.baseURL = 'https://api.staging.product7.io/api/v1';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('[APIService] Using API URL:', this.baseURL);
|
|
102
|
+
|
|
103
|
+
// Try to load existing session from localStorage
|
|
104
|
+
this._loadStoredSession();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async init(userContext = null) {
|
|
108
|
+
console.log('[APIService] Starting initialization...');
|
|
109
|
+
|
|
110
|
+
if (userContext) {
|
|
111
|
+
this.userContext = userContext;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check if we have a valid session token
|
|
115
|
+
if (this.isSessionValid()) {
|
|
116
|
+
console.log('[APIService] Found valid existing session');
|
|
117
|
+
return { sessionToken: this.sessionToken };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Try to get user context from various sources if not provided
|
|
121
|
+
if (!this.userContext) {
|
|
122
|
+
console.log(
|
|
123
|
+
'[APIService] No user context provided, attempting auto-detection...'
|
|
124
|
+
);
|
|
125
|
+
this.userContext = this._getUserContextFromStorage();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!this.userContext || !this.workspace) {
|
|
129
|
+
const error = `Missing ${!this.workspace ? 'workspace' : 'user context'} for initialization`;
|
|
130
|
+
console.error('[APIService]', error);
|
|
131
|
+
throw new APIError(400, error);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log('[APIService] User context detected:', this.userContext);
|
|
135
|
+
|
|
136
|
+
const payload = {
|
|
137
|
+
workspace: this.workspace,
|
|
138
|
+
user: this.userContext,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
console.log(
|
|
142
|
+
'[APIService] Making init request to:',
|
|
143
|
+
`${this.baseURL}/widget/init`
|
|
144
|
+
);
|
|
145
|
+
console.log('[APIService] Payload:', payload);
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const response = await this._makeRequest('/widget/init', {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body: JSON.stringify(payload),
|
|
151
|
+
headers: {
|
|
152
|
+
'Content-Type': 'application/json',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
console.log('[APIService] Init response:', response);
|
|
157
|
+
|
|
158
|
+
// Store session token and expiry
|
|
159
|
+
this.sessionToken = response.session_token;
|
|
160
|
+
this.sessionExpiry = new Date(Date.now() + response.expires_in * 1000);
|
|
161
|
+
|
|
162
|
+
// Store session in localStorage for persistence
|
|
163
|
+
this._storeSession();
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
sessionToken: this.sessionToken,
|
|
167
|
+
config: response.config || {},
|
|
168
|
+
expiresIn: response.expires_in,
|
|
169
|
+
};
|
|
170
|
+
} catch (error) {
|
|
171
|
+
console.error('[APIService] Init failed:', error);
|
|
172
|
+
throw new APIError(
|
|
173
|
+
error.status || 500,
|
|
174
|
+
`Failed to initialize widget: ${error.message}`,
|
|
175
|
+
error.response
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async submitFeedback(feedbackData) {
|
|
181
|
+
// Ensure we have a valid session
|
|
182
|
+
if (!this.isSessionValid()) {
|
|
183
|
+
await this.init();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!this.sessionToken) {
|
|
187
|
+
throw new APIError(401, 'No valid session token available');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const payload = {
|
|
191
|
+
board:
|
|
192
|
+
feedbackData.board_id || feedbackData.board || feedbackData.boardId,
|
|
193
|
+
title: feedbackData.title,
|
|
194
|
+
content: feedbackData.content,
|
|
195
|
+
attachments: feedbackData.attachments || [],
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const response = await this._makeRequest('/widget/feedback', {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
body: JSON.stringify(payload),
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json',
|
|
204
|
+
Authorization: `Bearer ${this.sessionToken}`,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return response;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
// If session expired, try to reinitialize once
|
|
211
|
+
if (error.status === 401) {
|
|
212
|
+
this.sessionToken = null;
|
|
213
|
+
this.sessionExpiry = null;
|
|
214
|
+
await this.init();
|
|
215
|
+
|
|
216
|
+
// Retry the request with new session
|
|
217
|
+
return this.submitFeedback(feedbackData);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
throw new APIError(
|
|
221
|
+
error.status || 500,
|
|
222
|
+
`Failed to submit feedback: ${error.message}`,
|
|
223
|
+
error.response
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
isSessionValid() {
|
|
229
|
+
return (
|
|
230
|
+
this.sessionToken && this.sessionExpiry && new Date() < this.sessionExpiry
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
setUserContext(userContext) {
|
|
235
|
+
this.userContext = userContext;
|
|
236
|
+
// Store in localStorage for persistence
|
|
237
|
+
if (typeof localStorage !== 'undefined') {
|
|
238
|
+
localStorage.setItem(
|
|
239
|
+
'feedbackSDK_userContext',
|
|
240
|
+
JSON.stringify(userContext)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getUserContext() {
|
|
246
|
+
return this.userContext;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
clearSession() {
|
|
250
|
+
this.sessionToken = null;
|
|
251
|
+
this.sessionExpiry = null;
|
|
252
|
+
if (typeof localStorage !== 'undefined') {
|
|
253
|
+
localStorage.removeItem('feedbackSDK_session');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_getUserContextFromStorage() {
|
|
258
|
+
if (typeof localStorage === 'undefined') return null;
|
|
259
|
+
|
|
260
|
+
console.log('[APIService] Attempting to detect user from storage...');
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
// Try to get from feedbackSDK specific storage first
|
|
264
|
+
const stored = localStorage.getItem('feedbackSDK_userContext');
|
|
265
|
+
if (stored) {
|
|
266
|
+
console.log('[APIService] Found user context in feedbackSDK storage');
|
|
267
|
+
return JSON.parse(stored);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Try to get from window object (if set by parent application)
|
|
271
|
+
if (typeof window !== 'undefined' && window.FeedbackSDKUserContext) {
|
|
272
|
+
console.log(
|
|
273
|
+
'[APIService] Found user context in window.FeedbackSDKUserContext'
|
|
274
|
+
);
|
|
275
|
+
return window.FeedbackSDKUserContext;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check window.currentUser
|
|
279
|
+
if (typeof window !== 'undefined' && window.currentUser) {
|
|
280
|
+
console.log('[APIService] Found user context in window.currentUser');
|
|
281
|
+
return this._mapUserData(window.currentUser);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Try to extract from existing session storage
|
|
285
|
+
const authSources = ['auth', 'currentUser', 'userSession', 'user'];
|
|
286
|
+
|
|
287
|
+
for (const key of authSources) {
|
|
288
|
+
const sessionData = localStorage.getItem(key);
|
|
289
|
+
if (sessionData) {
|
|
290
|
+
console.log(`[APIService] Found data in localStorage.${key}`);
|
|
291
|
+
const parsed = JSON.parse(sessionData);
|
|
292
|
+
|
|
293
|
+
// Handle nested user data (like {user: {...}, token: ...})
|
|
294
|
+
const userData = parsed.user || parsed;
|
|
295
|
+
|
|
296
|
+
if (userData && (userData.id || userData.user_id || userData.email)) {
|
|
297
|
+
console.log('[APIService] Successfully mapped user data');
|
|
298
|
+
return this._mapUserData(userData);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log('[APIService] No user context found in any storage location');
|
|
304
|
+
return null;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.warn(
|
|
307
|
+
'[FeedbackSDK] Failed to load user context from storage:',
|
|
308
|
+
error
|
|
309
|
+
);
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
_mapUserData(userData) {
|
|
315
|
+
console.log('[APIService] Mapping user data:', userData);
|
|
316
|
+
|
|
317
|
+
const mapped = {
|
|
318
|
+
user_id: userData.id || userData.user_id || userData.userId,
|
|
319
|
+
email: userData.email,
|
|
320
|
+
name: userData.name || userData.displayName || userData.full_name,
|
|
321
|
+
custom_fields: {},
|
|
322
|
+
company: {},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Map company data if available
|
|
326
|
+
if (userData.company || userData.organization) {
|
|
327
|
+
const company = userData.company || userData.organization;
|
|
328
|
+
mapped.company = {
|
|
329
|
+
id: company.id || company.company_id,
|
|
330
|
+
name: company.name || company.company_name,
|
|
331
|
+
monthly_spend: company.monthly_spend || company.spend,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Map any additional custom fields
|
|
336
|
+
const customFieldKeys = ['plan', 'role', 'tier', 'subscription'];
|
|
337
|
+
customFieldKeys.forEach((key) => {
|
|
338
|
+
if (userData[key]) {
|
|
339
|
+
mapped.custom_fields[key] = userData[key];
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
console.log('[APIService] Mapped result:', mapped);
|
|
344
|
+
return mapped;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
_storeSession() {
|
|
348
|
+
if (typeof localStorage === 'undefined') return;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const sessionData = {
|
|
352
|
+
token: this.sessionToken,
|
|
353
|
+
expiry: this.sessionExpiry.toISOString(),
|
|
354
|
+
workspace: this.workspace,
|
|
355
|
+
};
|
|
356
|
+
localStorage.setItem('feedbackSDK_session', JSON.stringify(sessionData));
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.warn('[FeedbackSDK] Failed to store session:', error);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_loadStoredSession() {
|
|
363
|
+
if (typeof localStorage === 'undefined') return false;
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const stored = localStorage.getItem('feedbackSDK_session');
|
|
367
|
+
if (!stored) return false;
|
|
368
|
+
|
|
369
|
+
const sessionData = JSON.parse(stored);
|
|
370
|
+
this.sessionToken = sessionData.token;
|
|
371
|
+
this.sessionExpiry = new Date(sessionData.expiry);
|
|
372
|
+
|
|
373
|
+
return this.isSessionValid();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
console.warn('[FeedbackSDK] Failed to load stored session:', error);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async _makeRequest(endpoint, options = {}) {
|
|
381
|
+
const url = `${this.baseURL}${endpoint}`;
|
|
382
|
+
console.log('[APIService] Making request to:', url);
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
const response = await fetch(url, options);
|
|
386
|
+
|
|
387
|
+
if (!response.ok) {
|
|
388
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
389
|
+
let responseData = null;
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
responseData = await response.json();
|
|
393
|
+
errorMessage =
|
|
394
|
+
responseData.message || responseData.error || errorMessage;
|
|
395
|
+
} catch (e) {
|
|
396
|
+
errorMessage = (await response.text()) || errorMessage;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
throw new APIError(response.status, errorMessage, responseData);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const contentType = response.headers.get('content-type');
|
|
403
|
+
if (contentType && contentType.includes('application/json')) {
|
|
404
|
+
return await response.json();
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return await response.text();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if (error instanceof APIError) {
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Network or other errors
|
|
414
|
+
throw new APIError(0, error.message, null);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
class EventBus {
|
|
420
|
+
constructor() {
|
|
421
|
+
this.events = new Map();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
on(event, callback) {
|
|
425
|
+
if (!this.events.has(event)) {
|
|
426
|
+
this.events.set(event, []);
|
|
427
|
+
}
|
|
428
|
+
this.events.get(event).push(callback);
|
|
429
|
+
|
|
430
|
+
return () => this.off(event, callback);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
off(event, callback) {
|
|
434
|
+
const callbacks = this.events.get(event);
|
|
435
|
+
if (callbacks) {
|
|
436
|
+
const index = callbacks.indexOf(callback);
|
|
437
|
+
if (index > -1) {
|
|
438
|
+
callbacks.splice(index, 1);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
emit(event, data) {
|
|
444
|
+
const callbacks = this.events.get(event);
|
|
445
|
+
if (callbacks) {
|
|
446
|
+
callbacks.forEach((callback) => {
|
|
447
|
+
try {
|
|
448
|
+
callback(data);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
console.error('[FeedbackSDK] Event callback error:', error);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
once(event, callback) {
|
|
457
|
+
const unsubscribe = this.on(event, (data) => {
|
|
458
|
+
callback(data);
|
|
459
|
+
unsubscribe();
|
|
460
|
+
});
|
|
461
|
+
return unsubscribe;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
clear() {
|
|
465
|
+
this.events.clear();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
getListenerCount(event) {
|
|
469
|
+
const callbacks = this.events.get(event);
|
|
470
|
+
return callbacks ? callbacks.length : 0;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function generateId(prefix = 'feedback') {
|
|
475
|
+
const timestamp = Date.now();
|
|
476
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
477
|
+
return `${prefix}_${timestamp}_${random}`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function deepMerge(target, source) {
|
|
481
|
+
const result = { ...target };
|
|
482
|
+
|
|
483
|
+
for (const key in source) {
|
|
484
|
+
if (source.hasOwnProperty(key)) {
|
|
485
|
+
if (
|
|
486
|
+
source[key] &&
|
|
487
|
+
typeof source[key] === 'object' &&
|
|
488
|
+
!Array.isArray(source[key])
|
|
489
|
+
) {
|
|
490
|
+
result[key] = deepMerge(target[key] || {}, source[key]);
|
|
491
|
+
} else {
|
|
492
|
+
result[key] = source[key];
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return result;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function debounce(func, wait) {
|
|
501
|
+
let timeout;
|
|
502
|
+
return function executedFunction(...args) {
|
|
503
|
+
const later = () => {
|
|
504
|
+
clearTimeout(timeout);
|
|
505
|
+
func(...args);
|
|
506
|
+
};
|
|
507
|
+
clearTimeout(timeout);
|
|
508
|
+
timeout = setTimeout(later, wait);
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function throttle(func, limit) {
|
|
513
|
+
let lastFunc;
|
|
514
|
+
let lastRan;
|
|
515
|
+
return function (...args) {
|
|
516
|
+
if (!lastRan) {
|
|
517
|
+
func(...args);
|
|
518
|
+
lastRan = Date.now();
|
|
519
|
+
} else {
|
|
520
|
+
clearTimeout(lastFunc);
|
|
521
|
+
lastFunc = setTimeout(
|
|
522
|
+
() => {
|
|
523
|
+
if (Date.now() - lastRan >= limit) {
|
|
524
|
+
func(...args);
|
|
525
|
+
lastRan = Date.now();
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
limit - (Date.now() - lastRan)
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function isValidEmail(email) {
|
|
535
|
+
if (!email || typeof email !== 'string') return false;
|
|
536
|
+
|
|
537
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
538
|
+
return emailRegex.test(email.trim());
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function sanitizeHTML(str) {
|
|
542
|
+
if (!str || typeof str !== 'string') return '';
|
|
543
|
+
|
|
544
|
+
const div = document.createElement('div');
|
|
545
|
+
div.textContent = str;
|
|
546
|
+
return div.innerHTML;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function getCSSProperty(element, property, fallback = '') {
|
|
550
|
+
if (!element || !property) return fallback;
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
const style = window.getComputedStyle(element);
|
|
554
|
+
return style.getPropertyValue(property) || fallback;
|
|
555
|
+
} catch (error) {
|
|
556
|
+
return fallback;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function isInViewport(element) {
|
|
561
|
+
if (!element) return false;
|
|
562
|
+
|
|
563
|
+
const rect = element.getBoundingClientRect();
|
|
564
|
+
return (
|
|
565
|
+
rect.top >= 0 &&
|
|
566
|
+
rect.left >= 0 &&
|
|
567
|
+
rect.bottom <=
|
|
568
|
+
(window.innerHeight || document.documentElement.clientHeight) &&
|
|
569
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function scrollToElement(element, options = {}) {
|
|
574
|
+
if (!element) return;
|
|
575
|
+
|
|
576
|
+
const defaultOptions = {
|
|
577
|
+
behavior: 'smooth',
|
|
578
|
+
block: 'center',
|
|
579
|
+
inline: 'nearest',
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
element.scrollIntoView({ ...defaultOptions, ...options });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function getBrowserInfo() {
|
|
586
|
+
const userAgent = navigator.userAgent;
|
|
587
|
+
const platform = navigator.platform;
|
|
588
|
+
|
|
589
|
+
return {
|
|
590
|
+
userAgent,
|
|
591
|
+
platform,
|
|
592
|
+
language: navigator.language || navigator.userLanguage,
|
|
593
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
594
|
+
screenResolution: `${screen.width}x${screen.height}`,
|
|
595
|
+
windowSize: `${window.innerWidth}x${window.innerHeight}`,
|
|
596
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function formatFileSize(bytes) {
|
|
601
|
+
if (bytes === 0) return '0 Bytes';
|
|
602
|
+
|
|
603
|
+
const k = 1024;
|
|
604
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
605
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
606
|
+
|
|
607
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function delay(ms) {
|
|
611
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function safeJsonParse(str, fallback = null) {
|
|
615
|
+
try {
|
|
616
|
+
return JSON.parse(str);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
return fallback;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function escapeRegex(string) {
|
|
623
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function getNestedProperty(obj, path, defaultValue = undefined) {
|
|
627
|
+
if (!obj || !path) return defaultValue;
|
|
628
|
+
|
|
629
|
+
const keys = path.split('.');
|
|
630
|
+
let current = obj;
|
|
631
|
+
|
|
632
|
+
for (const key of keys) {
|
|
633
|
+
if (current === null || current === undefined || !(key in current)) {
|
|
634
|
+
return defaultValue;
|
|
635
|
+
}
|
|
636
|
+
current = current[key];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return current;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function setNestedProperty(obj, path, value) {
|
|
643
|
+
if (!obj || !path) return obj;
|
|
644
|
+
|
|
645
|
+
const keys = path.split('.');
|
|
646
|
+
const lastKey = keys.pop();
|
|
647
|
+
let current = obj;
|
|
648
|
+
|
|
649
|
+
for (const key of keys) {
|
|
650
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
651
|
+
current[key] = {};
|
|
652
|
+
}
|
|
653
|
+
current = current[key];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
current[lastKey] = value;
|
|
657
|
+
return obj;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function isBrowser() {
|
|
661
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isMobile() {
|
|
665
|
+
if (!isBrowser()) return false;
|
|
666
|
+
|
|
667
|
+
return (
|
|
668
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
669
|
+
navigator.userAgent
|
|
670
|
+
) || window.innerWidth <= 768
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function getCurrentTimestamp() {
|
|
675
|
+
return new Date().toISOString();
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function validateConfig(config, required = []) {
|
|
679
|
+
const missing = [];
|
|
680
|
+
|
|
681
|
+
for (const key of required) {
|
|
682
|
+
if (!config[key]) {
|
|
683
|
+
missing.push(key);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (missing.length > 0) {
|
|
688
|
+
throw new Error(`Missing required configuration: ${missing.join(', ')}`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
var helpers = /*#__PURE__*/Object.freeze({
|
|
695
|
+
__proto__: null,
|
|
696
|
+
debounce: debounce,
|
|
697
|
+
deepMerge: deepMerge,
|
|
698
|
+
delay: delay,
|
|
699
|
+
escapeRegex: escapeRegex,
|
|
700
|
+
formatFileSize: formatFileSize,
|
|
701
|
+
generateId: generateId,
|
|
702
|
+
getBrowserInfo: getBrowserInfo,
|
|
703
|
+
getCSSProperty: getCSSProperty,
|
|
704
|
+
getCurrentTimestamp: getCurrentTimestamp,
|
|
705
|
+
getNestedProperty: getNestedProperty,
|
|
706
|
+
isBrowser: isBrowser,
|
|
707
|
+
isInViewport: isInViewport,
|
|
708
|
+
isMobile: isMobile,
|
|
709
|
+
isValidEmail: isValidEmail,
|
|
710
|
+
safeJsonParse: safeJsonParse,
|
|
711
|
+
sanitizeHTML: sanitizeHTML,
|
|
712
|
+
scrollToElement: scrollToElement,
|
|
713
|
+
setNestedProperty: setNestedProperty,
|
|
714
|
+
throttle: throttle,
|
|
715
|
+
validateConfig: validateConfig
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
class BaseWidget {
|
|
719
|
+
constructor(options = {}) {
|
|
720
|
+
this.id = options.id;
|
|
721
|
+
this.sdk = options.sdk;
|
|
722
|
+
this.apiService = options.apiService;
|
|
723
|
+
this.type = options.type || 'base';
|
|
724
|
+
|
|
725
|
+
this.options = {
|
|
726
|
+
container: null,
|
|
727
|
+
position: this.sdk.config.position,
|
|
728
|
+
theme: this.sdk.config.theme,
|
|
729
|
+
boardId: this.sdk.config.boardId,
|
|
730
|
+
autoShow: false,
|
|
731
|
+
customStyles: {},
|
|
732
|
+
...options,
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
this.element = null;
|
|
736
|
+
this.modalElement = null;
|
|
737
|
+
this.mounted = false;
|
|
738
|
+
this.destroyed = false;
|
|
739
|
+
|
|
740
|
+
this.state = {
|
|
741
|
+
isOpen: false,
|
|
742
|
+
isSubmitting: false,
|
|
743
|
+
title: '',
|
|
744
|
+
content: '',
|
|
745
|
+
email: '',
|
|
746
|
+
attachments: [],
|
|
747
|
+
errors: {},
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
this._bindMethods();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
mount(container) {
|
|
754
|
+
if (this.mounted || this.destroyed) return this;
|
|
755
|
+
|
|
756
|
+
if (typeof container === 'string') {
|
|
757
|
+
container = document.querySelector(container);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (!container) {
|
|
761
|
+
container = document.body;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this.container = container;
|
|
765
|
+
this.element = this._render();
|
|
766
|
+
this.container.appendChild(this.element);
|
|
767
|
+
|
|
768
|
+
this.mounted = true;
|
|
769
|
+
this._attachEvents();
|
|
770
|
+
this.onMount();
|
|
771
|
+
|
|
772
|
+
if (this.options.autoShow) {
|
|
773
|
+
this.show();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.sdk.eventBus.emit('widget:mounted', { widget: this });
|
|
777
|
+
return this;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
show() {
|
|
781
|
+
if (this.element) {
|
|
782
|
+
this.element.style.display = 'block';
|
|
783
|
+
}
|
|
784
|
+
return this;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
hide() {
|
|
788
|
+
if (this.element) {
|
|
789
|
+
this.element.style.display = 'none';
|
|
790
|
+
}
|
|
791
|
+
return this;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
openModal() {
|
|
795
|
+
this.state.isOpen = true;
|
|
796
|
+
this._renderModal();
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
closeModal() {
|
|
800
|
+
this.state.isOpen = false;
|
|
801
|
+
if (this.modalElement) {
|
|
802
|
+
this.modalElement.remove();
|
|
803
|
+
this.modalElement = null;
|
|
804
|
+
}
|
|
805
|
+
this._resetForm();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async submitFeedback() {
|
|
809
|
+
if (this.state.isSubmitting) return;
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
this.state.isSubmitting = true;
|
|
813
|
+
this._updateSubmitButton();
|
|
814
|
+
|
|
815
|
+
const payload = {
|
|
816
|
+
title: this.state.title || 'Feedback',
|
|
817
|
+
content: this.state.content,
|
|
818
|
+
email: this.state.email,
|
|
819
|
+
board_id: this.options.boardId,
|
|
820
|
+
attachments: this.state.attachments,
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (!this.state.content.trim()) {
|
|
824
|
+
this._showError('Please enter your feedback message.');
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
const response = await this.apiService.submitFeedback(payload);
|
|
829
|
+
|
|
830
|
+
this._showSuccessMessage();
|
|
831
|
+
this.closeModal();
|
|
832
|
+
|
|
833
|
+
this.sdk.eventBus.emit('feedback:submitted', {
|
|
834
|
+
widget: this,
|
|
835
|
+
feedback: response,
|
|
836
|
+
});
|
|
837
|
+
} catch (error) {
|
|
838
|
+
this._showError('Failed to submit feedback. Please try again.');
|
|
839
|
+
this.sdk.eventBus.emit('feedback:error', { widget: this, error });
|
|
840
|
+
} finally {
|
|
841
|
+
this.state.isSubmitting = false;
|
|
842
|
+
this._updateSubmitButton();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
handleConfigUpdate(newConfig) {
|
|
847
|
+
this.options.theme = newConfig.theme;
|
|
848
|
+
if (this.element) {
|
|
849
|
+
this._updateTheme();
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
destroy() {
|
|
854
|
+
if (this.destroyed) return;
|
|
855
|
+
|
|
856
|
+
this.onDestroy();
|
|
857
|
+
this.closeModal();
|
|
858
|
+
|
|
859
|
+
if (this.element && this.element.parentNode) {
|
|
860
|
+
this.element.parentNode.removeChild(this.element);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
this.destroyed = true;
|
|
864
|
+
this.mounted = false;
|
|
865
|
+
this.sdk.eventBus.emit('widget:destroyed', { widget: this });
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
onMount() {}
|
|
869
|
+
onDestroy() {}
|
|
870
|
+
|
|
871
|
+
_render() {
|
|
872
|
+
throw new Error('_render() must be implemented by concrete widget');
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
_attachEvents() {
|
|
876
|
+
// Override in concrete widgets
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
_bindMethods() {
|
|
880
|
+
this.openModal = this.openModal.bind(this);
|
|
881
|
+
this.closeModal = this.closeModal.bind(this);
|
|
882
|
+
this.submitFeedback = this.submitFeedback.bind(this);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
_renderModal() {
|
|
886
|
+
if (this.modalElement) return;
|
|
887
|
+
|
|
888
|
+
this.modalElement = document.createElement('div');
|
|
889
|
+
this.modalElement.className = `feedback-modal theme-${this.options.theme}`;
|
|
890
|
+
this.modalElement.innerHTML = this._getModalHTML();
|
|
891
|
+
|
|
892
|
+
document.body.appendChild(this.modalElement);
|
|
893
|
+
this._attachModalEvents();
|
|
894
|
+
|
|
895
|
+
const firstInput = this.modalElement.querySelector('input, textarea');
|
|
896
|
+
if (firstInput) {
|
|
897
|
+
setTimeout(() => firstInput.focus(), 100);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
_getModalHTML() {
|
|
902
|
+
return `
|
|
903
|
+
<div class="feedback-modal-overlay">
|
|
904
|
+
<div class="feedback-modal-content">
|
|
905
|
+
<div class="feedback-modal-header">
|
|
906
|
+
<h3>Send Feedback</h3>
|
|
907
|
+
<button class="feedback-modal-close" type="button">×</button>
|
|
908
|
+
</div>
|
|
909
|
+
<form class="feedback-form">
|
|
910
|
+
<div class="feedback-form-group">
|
|
911
|
+
<label for="feedback-title-${this.id}">Title</label>
|
|
912
|
+
<input
|
|
913
|
+
type="text"
|
|
914
|
+
id="feedback-title-${this.id}"
|
|
915
|
+
name="title"
|
|
916
|
+
placeholder="Brief description of your feedback"
|
|
917
|
+
value="${this.state.title}"
|
|
918
|
+
/>
|
|
919
|
+
</div>
|
|
920
|
+
<div class="feedback-form-group">
|
|
921
|
+
<label for="feedback-content-${this.id}">Message *</label>
|
|
922
|
+
<textarea
|
|
923
|
+
id="feedback-content-${this.id}"
|
|
924
|
+
name="content"
|
|
925
|
+
placeholder="Tell us more about your feedback..."
|
|
926
|
+
required
|
|
927
|
+
>${this.state.content}</textarea>
|
|
928
|
+
</div>
|
|
929
|
+
<div class="feedback-form-actions">
|
|
930
|
+
<button type="button" class="feedback-btn feedback-btn-cancel">Cancel</button>
|
|
931
|
+
<button type="submit" class="feedback-btn feedback-btn-submit">
|
|
932
|
+
${this.state.isSubmitting ? 'Sending...' : 'Send Feedback'}
|
|
933
|
+
</button>
|
|
934
|
+
</div>
|
|
935
|
+
<div class="feedback-error" style="display: none;"></div>
|
|
936
|
+
</form>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
`;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
_attachModalEvents() {
|
|
943
|
+
const modal = this.modalElement;
|
|
944
|
+
|
|
945
|
+
modal
|
|
946
|
+
.querySelector('.feedback-modal-close')
|
|
947
|
+
.addEventListener('click', this.closeModal);
|
|
948
|
+
modal
|
|
949
|
+
.querySelector('.feedback-btn-cancel')
|
|
950
|
+
.addEventListener('click', this.closeModal);
|
|
951
|
+
modal
|
|
952
|
+
.querySelector('.feedback-modal-overlay')
|
|
953
|
+
.addEventListener('click', (e) => {
|
|
954
|
+
if (e.target === e.currentTarget) {
|
|
955
|
+
this.closeModal();
|
|
956
|
+
}
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
const form = modal.querySelector('.feedback-form');
|
|
960
|
+
form.addEventListener('submit', (e) => {
|
|
961
|
+
e.preventDefault();
|
|
962
|
+
this.submitFeedback();
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
modal
|
|
966
|
+
.querySelector('input[name="title"]')
|
|
967
|
+
.addEventListener('input', (e) => {
|
|
968
|
+
this.state.title = e.target.value;
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
modal
|
|
972
|
+
.querySelector('textarea[name="content"]')
|
|
973
|
+
.addEventListener('input', (e) => {
|
|
974
|
+
this.state.content = e.target.value;
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
_updateSubmitButton() {
|
|
979
|
+
if (this.modalElement) {
|
|
980
|
+
const submitBtn = this.modalElement.querySelector('.feedback-btn-submit');
|
|
981
|
+
if (submitBtn) {
|
|
982
|
+
submitBtn.textContent = this.state.isSubmitting
|
|
983
|
+
? 'Sending...'
|
|
984
|
+
: 'Send Feedback';
|
|
985
|
+
submitBtn.disabled = this.state.isSubmitting;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
_showError(message) {
|
|
991
|
+
if (this.modalElement) {
|
|
992
|
+
const errorElement = this.modalElement.querySelector('.feedback-error');
|
|
993
|
+
errorElement.textContent = message;
|
|
994
|
+
errorElement.style.display = 'block';
|
|
995
|
+
setTimeout(() => {
|
|
996
|
+
if (errorElement) {
|
|
997
|
+
errorElement.style.display = 'none';
|
|
998
|
+
}
|
|
999
|
+
}, 5000);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
_showSuccessMessage() {
|
|
1004
|
+
const notification = document.createElement('div');
|
|
1005
|
+
notification.className = 'feedback-success-notification';
|
|
1006
|
+
notification.innerHTML = `
|
|
1007
|
+
<div class="feedback-success-content">
|
|
1008
|
+
<span>✓ Feedback submitted successfully!</span>
|
|
1009
|
+
<button class="feedback-success-close">×</button>
|
|
1010
|
+
</div>
|
|
1011
|
+
`;
|
|
1012
|
+
|
|
1013
|
+
document.body.appendChild(notification);
|
|
1014
|
+
|
|
1015
|
+
setTimeout(() => {
|
|
1016
|
+
if (notification.parentNode) {
|
|
1017
|
+
notification.parentNode.removeChild(notification);
|
|
1018
|
+
}
|
|
1019
|
+
}, 3000);
|
|
1020
|
+
|
|
1021
|
+
notification
|
|
1022
|
+
.querySelector('.feedback-success-close')
|
|
1023
|
+
.addEventListener('click', () => {
|
|
1024
|
+
if (notification.parentNode) {
|
|
1025
|
+
notification.parentNode.removeChild(notification);
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
_resetForm() {
|
|
1031
|
+
this.state.title = '';
|
|
1032
|
+
this.state.content = '';
|
|
1033
|
+
this.state.email = '';
|
|
1034
|
+
this.state.errors = {};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
_updateTheme() {
|
|
1038
|
+
if (this.element) {
|
|
1039
|
+
this.element.className = this.element.className.replace(
|
|
1040
|
+
/theme-\w+/,
|
|
1041
|
+
`theme-${this.options.theme}`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
if (this.modalElement) {
|
|
1045
|
+
this.modalElement.className = this.modalElement.className.replace(
|
|
1046
|
+
/theme-\w+/,
|
|
1047
|
+
`theme-${this.options.theme}`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
class ButtonWidget extends BaseWidget {
|
|
1054
|
+
constructor(options) {
|
|
1055
|
+
super({ ...options, type: 'button' });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
_render() {
|
|
1059
|
+
const button = document.createElement('div');
|
|
1060
|
+
button.className = `feedback-widget feedback-widget-button theme-${this.options.theme} position-${this.options.position}`;
|
|
1061
|
+
button.innerHTML = `
|
|
1062
|
+
<button class="feedback-trigger-btn" type="button">
|
|
1063
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="currentColor">
|
|
1064
|
+
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/>
|
|
1065
|
+
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/>
|
|
1066
|
+
</svg>
|
|
1067
|
+
Feedback
|
|
1068
|
+
</button>
|
|
1069
|
+
`;
|
|
1070
|
+
|
|
1071
|
+
if (this.options.customStyles) {
|
|
1072
|
+
Object.assign(button.style, this.options.customStyles);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return button;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
_attachEvents() {
|
|
1079
|
+
const button = this.element.querySelector('.feedback-trigger-btn');
|
|
1080
|
+
button.addEventListener('click', this.openModal);
|
|
1081
|
+
|
|
1082
|
+
button.addEventListener('mouseenter', () => {
|
|
1083
|
+
if (!this.state.isSubmitting) {
|
|
1084
|
+
button.style.transform = 'translateY(-2px)';
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
button.addEventListener('mouseleave', () => {
|
|
1089
|
+
button.style.transform = 'translateY(0)';
|
|
1090
|
+
});
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
updateText(text) {
|
|
1094
|
+
const button = this.element?.querySelector('.feedback-trigger-btn');
|
|
1095
|
+
if (button) {
|
|
1096
|
+
const textNode = button.childNodes[button.childNodes.length - 1];
|
|
1097
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
1098
|
+
textNode.textContent = text;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
updatePosition(position) {
|
|
1104
|
+
this.options.position = position;
|
|
1105
|
+
if (this.element) {
|
|
1106
|
+
this.element.className = this.element.className.replace(
|
|
1107
|
+
/position-\w+-\w+/,
|
|
1108
|
+
`position-${position}`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
class InlineWidget extends BaseWidget {
|
|
1115
|
+
constructor(options) {
|
|
1116
|
+
super({ ...options, type: 'inline' });
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
_render() {
|
|
1120
|
+
const widget = document.createElement('div');
|
|
1121
|
+
widget.className = `feedback-widget feedback-widget-inline theme-${this.options.theme}`;
|
|
1122
|
+
widget.innerHTML = `
|
|
1123
|
+
<div class="feedback-inline-content">
|
|
1124
|
+
<h3>Send us your feedback</h3>
|
|
1125
|
+
<form class="feedback-inline-form">
|
|
1126
|
+
<div class="feedback-form-group">
|
|
1127
|
+
<input
|
|
1128
|
+
type="text"
|
|
1129
|
+
name="title"
|
|
1130
|
+
placeholder="Title (optional)"
|
|
1131
|
+
value="${this.state.title}"
|
|
1132
|
+
/>
|
|
1133
|
+
</div>
|
|
1134
|
+
<div class="feedback-form-group">
|
|
1135
|
+
<textarea
|
|
1136
|
+
name="content"
|
|
1137
|
+
placeholder="Your feedback..."
|
|
1138
|
+
required
|
|
1139
|
+
>${this.state.content}</textarea>
|
|
1140
|
+
</div>
|
|
1141
|
+
<div class="feedback-form-group">
|
|
1142
|
+
<input
|
|
1143
|
+
type="email"
|
|
1144
|
+
name="email"
|
|
1145
|
+
placeholder="Email (optional)"
|
|
1146
|
+
value="${this.state.email}"
|
|
1147
|
+
/>
|
|
1148
|
+
</div>
|
|
1149
|
+
<button type="submit" class="feedback-btn feedback-btn-submit">
|
|
1150
|
+
Send Feedback
|
|
1151
|
+
</button>
|
|
1152
|
+
<div class="feedback-error" style="display: none;"></div>
|
|
1153
|
+
</form>
|
|
1154
|
+
</div>
|
|
1155
|
+
`;
|
|
1156
|
+
|
|
1157
|
+
if (this.options.customStyles) {
|
|
1158
|
+
Object.assign(widget.style, this.options.customStyles);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return widget;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
_attachEvents() {
|
|
1165
|
+
const form = this.element.querySelector('.feedback-inline-form');
|
|
1166
|
+
|
|
1167
|
+
form.addEventListener('submit', (e) => {
|
|
1168
|
+
e.preventDefault();
|
|
1169
|
+
this.submitFeedback();
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
form.querySelector('input[name="title"]').addEventListener('input', (e) => {
|
|
1173
|
+
this.state.title = e.target.value;
|
|
1174
|
+
});
|
|
1175
|
+
|
|
1176
|
+
form
|
|
1177
|
+
.querySelector('textarea[name="content"]')
|
|
1178
|
+
.addEventListener('input', (e) => {
|
|
1179
|
+
this.state.content = e.target.value;
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
form.querySelector('input[name="email"]').addEventListener('input', (e) => {
|
|
1183
|
+
this.state.email = e.target.value;
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
openModal() {
|
|
1188
|
+
const textarea = this.element.querySelector('textarea[name="content"]');
|
|
1189
|
+
if (textarea) {
|
|
1190
|
+
textarea.focus();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
closeModal() {
|
|
1195
|
+
// Inline widget doesn't use modal
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
_showSuccessMessage() {
|
|
1199
|
+
const widget = this.element.querySelector('.feedback-inline-content');
|
|
1200
|
+
const originalContent = widget.innerHTML;
|
|
1201
|
+
|
|
1202
|
+
widget.innerHTML = `
|
|
1203
|
+
<div class="feedback-success">
|
|
1204
|
+
<div class="feedback-success-icon">✓</div>
|
|
1205
|
+
<h3>Thank you!</h3>
|
|
1206
|
+
<p>Your feedback has been submitted successfully.</p>
|
|
1207
|
+
<button class="feedback-btn feedback-btn-reset">Send Another</button>
|
|
1208
|
+
</div>
|
|
1209
|
+
`;
|
|
1210
|
+
|
|
1211
|
+
const resetBtn = widget.querySelector('.feedback-btn-reset');
|
|
1212
|
+
resetBtn.addEventListener('click', () => {
|
|
1213
|
+
widget.innerHTML = originalContent;
|
|
1214
|
+
this._attachEvents();
|
|
1215
|
+
this._resetForm();
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
_showError(message) {
|
|
1220
|
+
const errorElement = this.element.querySelector('.feedback-error');
|
|
1221
|
+
if (errorElement) {
|
|
1222
|
+
errorElement.textContent = message;
|
|
1223
|
+
errorElement.style.display = 'block';
|
|
1224
|
+
|
|
1225
|
+
setTimeout(() => {
|
|
1226
|
+
if (errorElement) {
|
|
1227
|
+
errorElement.style.display = 'none';
|
|
1228
|
+
}
|
|
1229
|
+
}, 5000);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
_updateSubmitButton() {
|
|
1234
|
+
const submitBtn = this.element.querySelector('.feedback-btn-submit');
|
|
1235
|
+
if (submitBtn) {
|
|
1236
|
+
submitBtn.textContent = this.state.isSubmitting
|
|
1237
|
+
? 'Sending...'
|
|
1238
|
+
: 'Send Feedback';
|
|
1239
|
+
submitBtn.disabled = this.state.isSubmitting;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
updateTitle(title) {
|
|
1244
|
+
const titleElement = this.element?.querySelector('h3');
|
|
1245
|
+
if (titleElement) {
|
|
1246
|
+
titleElement.textContent = title;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
setPlaceholder(field, placeholder) {
|
|
1251
|
+
const input = this.element?.querySelector(`[name="${field}"]`);
|
|
1252
|
+
if (input) {
|
|
1253
|
+
input.placeholder = placeholder;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
class TabWidget extends BaseWidget {
|
|
1259
|
+
constructor(options) {
|
|
1260
|
+
super({ ...options, type: 'tab' });
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
_render() {
|
|
1264
|
+
const tab = document.createElement('div');
|
|
1265
|
+
tab.className = `feedback-widget feedback-widget-tab theme-${this.options.theme} position-${this.options.position}`;
|
|
1266
|
+
tab.innerHTML = `
|
|
1267
|
+
<div class="feedback-tab-trigger">
|
|
1268
|
+
<span class="feedback-tab-text">Feedback</span>
|
|
1269
|
+
</div>
|
|
1270
|
+
`;
|
|
1271
|
+
|
|
1272
|
+
if (this.options.customStyles) {
|
|
1273
|
+
Object.assign(tab.style, this.options.customStyles);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
return tab;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
_attachEvents() {
|
|
1280
|
+
const tab = this.element.querySelector('.feedback-tab-trigger');
|
|
1281
|
+
tab.addEventListener('click', this.openModal);
|
|
1282
|
+
|
|
1283
|
+
tab.addEventListener('mouseenter', () => {
|
|
1284
|
+
if (!this.state.isSubmitting) {
|
|
1285
|
+
tab.style.transform = this._getHoverTransform();
|
|
1286
|
+
}
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
tab.addEventListener('mouseleave', () => {
|
|
1290
|
+
tab.style.transform = 'none';
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
_getHoverTransform() {
|
|
1295
|
+
const position = this.options.position;
|
|
1296
|
+
if (position.includes('right')) {
|
|
1297
|
+
return 'translateX(-5px)';
|
|
1298
|
+
} else if (position.includes('left')) {
|
|
1299
|
+
return 'translateX(5px)';
|
|
1300
|
+
}
|
|
1301
|
+
return 'none';
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
updateText(text) {
|
|
1305
|
+
const textElement = this.element?.querySelector('.feedback-tab-text');
|
|
1306
|
+
if (textElement) {
|
|
1307
|
+
textElement.textContent = text;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
updatePosition(position) {
|
|
1312
|
+
this.options.position = position;
|
|
1313
|
+
if (this.element) {
|
|
1314
|
+
this.element.className = this.element.className.replace(
|
|
1315
|
+
/position-\w+-\w+/,
|
|
1316
|
+
`position-${position}`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
class WidgetFactory {
|
|
1323
|
+
static widgets = new Map([
|
|
1324
|
+
['button', ButtonWidget],
|
|
1325
|
+
['tab', TabWidget],
|
|
1326
|
+
['inline', InlineWidget],
|
|
1327
|
+
]);
|
|
1328
|
+
|
|
1329
|
+
static register(type, WidgetClass) {
|
|
1330
|
+
if (typeof type !== 'string' || !type.trim()) {
|
|
1331
|
+
throw new SDKError('Widget type must be a non-empty string');
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
if (typeof WidgetClass !== 'function') {
|
|
1335
|
+
throw new SDKError('Widget class must be a constructor function');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
this.widgets.set(type, WidgetClass);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
static create(type, options = {}) {
|
|
1342
|
+
const WidgetClass = this.widgets.get(type);
|
|
1343
|
+
|
|
1344
|
+
if (!WidgetClass) {
|
|
1345
|
+
const availableTypes = Array.from(this.widgets.keys()).join(', ');
|
|
1346
|
+
throw new SDKError(
|
|
1347
|
+
`Unknown widget type: ${type}. Available types: ${availableTypes}`
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
try {
|
|
1352
|
+
return new WidgetClass(options);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
throw new SDKError(
|
|
1355
|
+
`Failed to create widget of type '${type}': ${error.message}`,
|
|
1356
|
+
error
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
static getAvailableTypes() {
|
|
1362
|
+
return Array.from(this.widgets.keys());
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
static isTypeRegistered(type) {
|
|
1366
|
+
return this.widgets.has(type);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
static unregister(type) {
|
|
1370
|
+
return this.widgets.delete(type);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
static clear() {
|
|
1374
|
+
this.widgets.clear();
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
static getWidgetClass(type) {
|
|
1378
|
+
return this.widgets.get(type);
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
class FeedbackSDK {
|
|
1383
|
+
constructor(config = {}) {
|
|
1384
|
+
this.config = this._validateAndMergeConfig(config);
|
|
1385
|
+
this.initialized = false;
|
|
1386
|
+
this.widgets = new Map();
|
|
1387
|
+
this.eventBus = new EventBus();
|
|
1388
|
+
|
|
1389
|
+
// Initialize API service
|
|
1390
|
+
this.apiService = new APIService({
|
|
1391
|
+
apiUrl: this.config.apiUrl,
|
|
1392
|
+
workspace: this.config.workspace,
|
|
1393
|
+
userContext: this.config.userContext,
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
this._bindMethods();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
async init() {
|
|
1400
|
+
if (this.initialized) {
|
|
1401
|
+
return { alreadyInitialized: true };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
try {
|
|
1405
|
+
// Initialize the API service (this will handle the /widget/init call)
|
|
1406
|
+
const initData = await this.apiService.init(this.config.userContext);
|
|
1407
|
+
|
|
1408
|
+
// Merge any server-provided config with local config
|
|
1409
|
+
if (initData.config) {
|
|
1410
|
+
this.config = deepMerge(this.config, initData.config);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
this.initialized = true;
|
|
1414
|
+
this.eventBus.emit('sdk:initialized', {
|
|
1415
|
+
config: this.config,
|
|
1416
|
+
sessionToken: initData.sessionToken,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
return {
|
|
1420
|
+
initialized: true,
|
|
1421
|
+
config: initData.config || {},
|
|
1422
|
+
sessionToken: initData.sessionToken,
|
|
1423
|
+
expiresIn: initData.expiresIn,
|
|
1424
|
+
};
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
this.eventBus.emit('sdk:error', { error });
|
|
1427
|
+
throw new SDKError(`Failed to initialize SDK: ${error.message}`, error);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
createWidget(type = 'button', options = {}) {
|
|
1432
|
+
if (!this.initialized) {
|
|
1433
|
+
throw new SDKError(
|
|
1434
|
+
'SDK must be initialized before creating widgets. Call init() first.'
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
const widgetId = generateId('widget');
|
|
1439
|
+
const widgetOptions = {
|
|
1440
|
+
id: widgetId,
|
|
1441
|
+
sdk: this,
|
|
1442
|
+
apiService: this.apiService,
|
|
1443
|
+
...this.config,
|
|
1444
|
+
...options,
|
|
1445
|
+
};
|
|
1446
|
+
|
|
1447
|
+
try {
|
|
1448
|
+
const widget = WidgetFactory.create(type, widgetOptions);
|
|
1449
|
+
this.widgets.set(widgetId, widget);
|
|
1450
|
+
|
|
1451
|
+
this.eventBus.emit('widget:created', { widget, type });
|
|
1452
|
+
return widget;
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
throw new SDKError(`Failed to create widget: ${error.message}`, error);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
getWidget(id) {
|
|
1459
|
+
return this.widgets.get(id);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
getAllWidgets() {
|
|
1463
|
+
return Array.from(this.widgets.values());
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
destroyWidget(id) {
|
|
1467
|
+
const widget = this.widgets.get(id);
|
|
1468
|
+
if (widget) {
|
|
1469
|
+
widget.destroy();
|
|
1470
|
+
this.widgets.delete(id);
|
|
1471
|
+
this.eventBus.emit('widget:removed', { widgetId: id });
|
|
1472
|
+
return true;
|
|
1473
|
+
}
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
destroyAllWidgets() {
|
|
1478
|
+
for (const widget of this.widgets.values()) {
|
|
1479
|
+
widget.destroy();
|
|
1480
|
+
}
|
|
1481
|
+
this.widgets.clear();
|
|
1482
|
+
this.eventBus.emit('widgets:cleared');
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
updateConfig(newConfig) {
|
|
1486
|
+
const oldConfig = { ...this.config };
|
|
1487
|
+
this.config = this._validateAndMergeConfig(newConfig, this.config);
|
|
1488
|
+
|
|
1489
|
+
// Update all existing widgets with new config
|
|
1490
|
+
for (const widget of this.widgets.values()) {
|
|
1491
|
+
widget.handleConfigUpdate(this.config);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
this.eventBus.emit('config:updated', {
|
|
1495
|
+
oldConfig,
|
|
1496
|
+
newConfig: this.config,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
setUserContext(userContext) {
|
|
1501
|
+
this.config.userContext = userContext;
|
|
1502
|
+
if (this.apiService) {
|
|
1503
|
+
this.apiService.setUserContext(userContext);
|
|
1504
|
+
}
|
|
1505
|
+
this.eventBus.emit('user:updated', { userContext });
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
getUserContext() {
|
|
1509
|
+
return (
|
|
1510
|
+
this.config.userContext ||
|
|
1511
|
+
(this.apiService ? this.apiService.getUserContext() : null)
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async reinitialize(newUserContext = null) {
|
|
1516
|
+
// Clear current session
|
|
1517
|
+
this.apiService.clearSession();
|
|
1518
|
+
this.initialized = false;
|
|
1519
|
+
|
|
1520
|
+
// Update user context if provided
|
|
1521
|
+
if (newUserContext) {
|
|
1522
|
+
this.setUserContext(newUserContext);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Reinitialize
|
|
1526
|
+
return this.init();
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
on(event, callback) {
|
|
1530
|
+
this.eventBus.on(event, callback);
|
|
1531
|
+
return this;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
off(event, callback) {
|
|
1535
|
+
this.eventBus.off(event, callback);
|
|
1536
|
+
return this;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
once(event, callback) {
|
|
1540
|
+
this.eventBus.once(event, callback);
|
|
1541
|
+
return this;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
emit(event, data) {
|
|
1545
|
+
this.eventBus.emit(event, data);
|
|
1546
|
+
return this;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
destroy() {
|
|
1550
|
+
this.destroyAllWidgets();
|
|
1551
|
+
this.eventBus.removeAllListeners();
|
|
1552
|
+
this.apiService.clearSession();
|
|
1553
|
+
this.initialized = false;
|
|
1554
|
+
this.eventBus.emit('sdk:destroyed');
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
_validateAndMergeConfig(newConfig, existingConfig = {}) {
|
|
1558
|
+
const defaultConfig = {
|
|
1559
|
+
apiUrl: null,
|
|
1560
|
+
workspace: null,
|
|
1561
|
+
userContext: null,
|
|
1562
|
+
position: 'bottom-right',
|
|
1563
|
+
theme: 'light',
|
|
1564
|
+
boardId: 'general',
|
|
1565
|
+
autoShow: true,
|
|
1566
|
+
debug: false,
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
const mergedConfig = deepMerge(
|
|
1570
|
+
deepMerge(defaultConfig, existingConfig),
|
|
1571
|
+
newConfig
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
// Validate required config
|
|
1575
|
+
const requiredFields = ['workspace'];
|
|
1576
|
+
const missingFields = requiredFields.filter(
|
|
1577
|
+
(field) => !mergedConfig[field]
|
|
1578
|
+
);
|
|
1579
|
+
|
|
1580
|
+
if (missingFields.length > 0) {
|
|
1581
|
+
throw new ConfigError(
|
|
1582
|
+
`Missing required configuration: ${missingFields.join(', ')}`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// Validate userContext structure if provided
|
|
1587
|
+
if (mergedConfig.userContext) {
|
|
1588
|
+
this._validateUserContext(mergedConfig.userContext);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
return mergedConfig;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
_validateUserContext(userContext) {
|
|
1595
|
+
if (!userContext.user_id && !userContext.email) {
|
|
1596
|
+
throw new ConfigError(
|
|
1597
|
+
'User context must include at least user_id or email'
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// Validate structure matches expected API format
|
|
1602
|
+
const validStructure = {
|
|
1603
|
+
user_id: 'string',
|
|
1604
|
+
email: 'string',
|
|
1605
|
+
name: 'string',
|
|
1606
|
+
custom_fields: 'object',
|
|
1607
|
+
company: 'object',
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
for (const [key, expectedType] of Object.entries(validStructure)) {
|
|
1611
|
+
if (userContext[key] && typeof userContext[key] !== expectedType) {
|
|
1612
|
+
throw new ConfigError(
|
|
1613
|
+
`User context field '${key}' must be of type '${expectedType}'`
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
_bindMethods() {
|
|
1620
|
+
this.createWidget = this.createWidget.bind(this);
|
|
1621
|
+
this.destroyWidget = this.destroyWidget.bind(this);
|
|
1622
|
+
this.updateConfig = this.updateConfig.bind(this);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// Static helper methods
|
|
1626
|
+
static create(config) {
|
|
1627
|
+
return new FeedbackSDK(config);
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
static async createAndInit(config) {
|
|
1631
|
+
const sdk = new FeedbackSDK(config);
|
|
1632
|
+
await sdk.init();
|
|
1633
|
+
return sdk;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Utility methods for external integrations
|
|
1637
|
+
static extractUserContextFromAuth(authData) {
|
|
1638
|
+
// Helper method to extract user context from common auth structures
|
|
1639
|
+
if (!authData) return null;
|
|
1640
|
+
|
|
1641
|
+
return {
|
|
1642
|
+
user_id: authData.sub || authData.id || authData.user_id,
|
|
1643
|
+
email: authData.email,
|
|
1644
|
+
name: authData.name || authData.display_name || authData.full_name,
|
|
1645
|
+
custom_fields: {
|
|
1646
|
+
role: authData.role,
|
|
1647
|
+
plan: authData.plan || authData.subscription?.plan,
|
|
1648
|
+
...(authData.custom_fields || {}),
|
|
1649
|
+
},
|
|
1650
|
+
company:
|
|
1651
|
+
authData.company || authData.organization
|
|
1652
|
+
? {
|
|
1653
|
+
id: authData.company?.id || authData.organization?.id,
|
|
1654
|
+
name: authData.company?.name || authData.organization?.name,
|
|
1655
|
+
monthly_spend: authData.company?.monthly_spend,
|
|
1656
|
+
}
|
|
1657
|
+
: undefined,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
const CSS_STYLES = `
|
|
1663
|
+
.feedback-widget {
|
|
1664
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
1665
|
+
font-size: 14px;
|
|
1666
|
+
line-height: 1.4;
|
|
1667
|
+
z-index: 999999;
|
|
1668
|
+
box-sizing: border-box;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
.feedback-widget *,
|
|
1672
|
+
.feedback-widget *::before,
|
|
1673
|
+
.feedback-widget *::after {
|
|
1674
|
+
box-sizing: border-box;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.feedback-widget-button {
|
|
1678
|
+
position: fixed;
|
|
1679
|
+
z-index: 999999;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
.feedback-widget-button.position-bottom-right {
|
|
1683
|
+
bottom: 20px;
|
|
1684
|
+
right: 20px;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
.feedback-widget-button.position-bottom-left {
|
|
1688
|
+
bottom: 20px;
|
|
1689
|
+
left: 20px;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
.feedback-widget-button.position-top-right {
|
|
1693
|
+
top: 20px;
|
|
1694
|
+
right: 20px;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
.feedback-widget-button.position-top-left {
|
|
1698
|
+
top: 20px;
|
|
1699
|
+
left: 20px;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.feedback-trigger-btn {
|
|
1703
|
+
position: relative;
|
|
1704
|
+
display: flex;
|
|
1705
|
+
align-items: center;
|
|
1706
|
+
justify-content: center;
|
|
1707
|
+
gap: 12px;
|
|
1708
|
+
height: 44px;
|
|
1709
|
+
overflow: hidden;
|
|
1710
|
+
border-radius: 0.5rem;
|
|
1711
|
+
border: none;
|
|
1712
|
+
padding: 10px 16px;
|
|
1713
|
+
font-size: 14px;
|
|
1714
|
+
font-weight: 500;
|
|
1715
|
+
font-family: inherit;
|
|
1716
|
+
cursor: pointer;
|
|
1717
|
+
transition: all 0.3s duration;
|
|
1718
|
+
color: white;
|
|
1719
|
+
background: #155EEF;
|
|
1720
|
+
box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.05);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.feedback-trigger-btn:hover:not(:disabled) {
|
|
1724
|
+
background: #004EEB;
|
|
1725
|
+
box-shadow: 0 1px 2px 0 rgba(16, 24, 40, 0.1);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
.feedback-trigger-btn:disabled {
|
|
1729
|
+
opacity: 0.7;
|
|
1730
|
+
cursor: not-allowed;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
.feedback-trigger-btn:focus-visible {
|
|
1734
|
+
outline: 2px solid #155EEF;
|
|
1735
|
+
outline-offset: 2px;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
.feedback-modal {
|
|
1739
|
+
position: fixed;
|
|
1740
|
+
top: 0;
|
|
1741
|
+
left: 0;
|
|
1742
|
+
right: 0;
|
|
1743
|
+
bottom: 0;
|
|
1744
|
+
z-index: 1000000;
|
|
1745
|
+
display: flex;
|
|
1746
|
+
align-items: center;
|
|
1747
|
+
justify-content: center;
|
|
1748
|
+
font-family: inherit;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
.feedback-modal-overlay {
|
|
1752
|
+
position: absolute;
|
|
1753
|
+
top: 0;
|
|
1754
|
+
left: 0;
|
|
1755
|
+
right: 0;
|
|
1756
|
+
bottom: 0;
|
|
1757
|
+
background: rgba(0, 0, 0, 0.5);
|
|
1758
|
+
display: flex;
|
|
1759
|
+
align-items: center;
|
|
1760
|
+
justify-content: center;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
.feedback-modal-content {
|
|
1764
|
+
background: white;
|
|
1765
|
+
border-radius: 8px;
|
|
1766
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
1767
|
+
min-width: 460px;
|
|
1768
|
+
max-width: 500px;
|
|
1769
|
+
width: 100%;
|
|
1770
|
+
padding: 16px;
|
|
1771
|
+
max-height: 85vh;
|
|
1772
|
+
overflow-y: hidden;
|
|
1773
|
+
position: relative;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
.feedback-modal.theme-dark .feedback-modal-content {
|
|
1777
|
+
background: #1F2937;
|
|
1778
|
+
color: white;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
.feedback-modal-header {
|
|
1782
|
+
display: flex;
|
|
1783
|
+
align-items: center;
|
|
1784
|
+
justify-content: space-between;
|
|
1785
|
+
padding: 16px;
|
|
1786
|
+
border-bottom: 1px solid #D1D5DB;
|
|
1787
|
+
flex-shrink: 0;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
.feedback-modal.theme-dark .feedback-modal-header {
|
|
1791
|
+
border-bottom-color: #374151;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
.feedback-modal-header h3 {
|
|
1795
|
+
margin: 0;
|
|
1796
|
+
font-size: 16px;
|
|
1797
|
+
font-weight: 600;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
.feedback-modal-close {
|
|
1801
|
+
background: none;
|
|
1802
|
+
border: none;
|
|
1803
|
+
font-size: 24px;
|
|
1804
|
+
cursor: pointer;
|
|
1805
|
+
color: #6B7280;
|
|
1806
|
+
padding: 0;
|
|
1807
|
+
width: 24px;
|
|
1808
|
+
height: 24px;
|
|
1809
|
+
display: flex;
|
|
1810
|
+
align-items: center;
|
|
1811
|
+
justify-content: center;
|
|
1812
|
+
transition: all 0.3s ease;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
.feedback-modal-close:hover {
|
|
1816
|
+
color: #374151;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
.feedback-modal-close:focus-visible {
|
|
1820
|
+
outline: 2px solid #155EEF;
|
|
1821
|
+
outline-offset: 2px;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
.feedback-modal.theme-dark .feedback-modal-close {
|
|
1825
|
+
color: #9CA3AF;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
.feedback-modal.theme-dark .feedback-modal-close:hover {
|
|
1829
|
+
color: #D1D5DB;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
.feedback-form {
|
|
1833
|
+
padding: 16px;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.feedback-form-group {
|
|
1837
|
+
display: flex;
|
|
1838
|
+
flex-direction: column;
|
|
1839
|
+
gap: 4px;
|
|
1840
|
+
margin-bottom: 12px;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
.feedback-form-group:last-child {
|
|
1844
|
+
margin-bottom: 0;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
.feedback-form-group label {
|
|
1848
|
+
font-size: 14px;
|
|
1849
|
+
font-weight: 500;
|
|
1850
|
+
line-height: 1.25;
|
|
1851
|
+
color: #374151;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
.feedback-modal.theme-dark .feedback-form-group label {
|
|
1855
|
+
color: #D1D5DB;
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
.feedback-form-group input {
|
|
1859
|
+
height: 40px;
|
|
1860
|
+
width: 100%;
|
|
1861
|
+
border-radius: 6px;
|
|
1862
|
+
border: 1px solid #D1D5DB;
|
|
1863
|
+
padding: 2px 12px;
|
|
1864
|
+
font-size: 14px;
|
|
1865
|
+
font-weight: 400;
|
|
1866
|
+
line-height: 1.25;
|
|
1867
|
+
color: #1F2937;
|
|
1868
|
+
font-family: inherit;
|
|
1869
|
+
outline: none;
|
|
1870
|
+
transition: all 0.2s ease;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
.feedback-form-group input::placeholder {
|
|
1874
|
+
font-size: 14px;
|
|
1875
|
+
color: #6B7280;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
.feedback-form-group input:focus {
|
|
1879
|
+
border-color: #84ADFF;
|
|
1880
|
+
box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.05), 0 0 0 3px rgba(41, 112, 255, 0.2);
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
.feedback-form-group input:focus-visible {
|
|
1884
|
+
outline: none;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.feedback-form-group textarea {
|
|
1888
|
+
min-height: 100px;
|
|
1889
|
+
width: 100%;
|
|
1890
|
+
resize: both;
|
|
1891
|
+
border-radius: 6px;
|
|
1892
|
+
border: 1px solid #D1D5DB;
|
|
1893
|
+
padding: 2px 12px;
|
|
1894
|
+
font-size: 14px;
|
|
1895
|
+
font-weight: 400;
|
|
1896
|
+
line-height: 1.25;
|
|
1897
|
+
color: #1F2937;
|
|
1898
|
+
font-family: inherit;
|
|
1899
|
+
outline: none;
|
|
1900
|
+
transition: all 0.2s ease;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
.feedback-form-group textarea::placeholder {
|
|
1904
|
+
font-size: 14px;
|
|
1905
|
+
color: #6B7280;
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
.feedback-form-group textarea:focus {
|
|
1909
|
+
border-color: #84ADFF;
|
|
1910
|
+
box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.05), 0 0 0 3px rgba(41, 112, 255, 0.2);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
.feedback-form-group textarea:focus-visible {
|
|
1914
|
+
outline: none;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
.feedback-modal.theme-dark .feedback-form-group input,
|
|
1918
|
+
.feedback-modal.theme-dark .feedback-form-group textarea {
|
|
1919
|
+
background: #374151;
|
|
1920
|
+
border-color: #4B5563;
|
|
1921
|
+
color: white;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
.feedback-btn {
|
|
1925
|
+
position: relative;
|
|
1926
|
+
display: inline-flex;
|
|
1927
|
+
align-items: center;
|
|
1928
|
+
justify-content: center;
|
|
1929
|
+
overflow: hidden;
|
|
1930
|
+
border-radius: 6px;
|
|
1931
|
+
border: none;
|
|
1932
|
+
height: 40px;
|
|
1933
|
+
padding: 2px 16px;
|
|
1934
|
+
font-size: 14px;
|
|
1935
|
+
font-weight: 500;
|
|
1936
|
+
font-family: inherit;
|
|
1937
|
+
cursor: pointer;
|
|
1938
|
+
transition: all 0.2s ease;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
.feedback-btn:disabled {
|
|
1942
|
+
opacity: 0.7;
|
|
1943
|
+
cursor: not-allowed;
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
.feedback-btn:focus-visible {
|
|
1947
|
+
outline: 2px solid #155EEF;
|
|
1948
|
+
outline-offset: 2px;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
.feedback-btn-submit {
|
|
1952
|
+
background: #155EEF;
|
|
1953
|
+
color: white;
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
.feedback-btn-submit:hover:not(:disabled) {
|
|
1957
|
+
background: #004EEB;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
.feedback-btn-cancel {
|
|
1961
|
+
background: transparent;
|
|
1962
|
+
color: #6B7280;
|
|
1963
|
+
border: 1px solid #D1D5DB;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
.feedback-btn-cancel:hover:not(:disabled) {
|
|
1967
|
+
background: #F9FAFB;
|
|
1968
|
+
border-color: #9CA3AF;
|
|
1969
|
+
color: #374151;
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
.feedback-modal.theme-dark .feedback-btn-cancel {
|
|
1973
|
+
color: #D1D5DB;
|
|
1974
|
+
border-color: #4B5563;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
.feedback-modal.theme-dark .feedback-btn-cancel:hover:not(:disabled) {
|
|
1978
|
+
background: #374151;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
.feedback-form-actions {
|
|
1982
|
+
display: flex;
|
|
1983
|
+
gap: 8px;
|
|
1984
|
+
justify-content: flex-end;
|
|
1985
|
+
margin-top: 16px;
|
|
1986
|
+
padding-top: 4px;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
.feedback-loading {
|
|
1990
|
+
width: 20px;
|
|
1991
|
+
height: 20px;
|
|
1992
|
+
border-radius: 50%;
|
|
1993
|
+
mask: radial-gradient(transparent 62%, white 65%);
|
|
1994
|
+
-webkit-mask: radial-gradient(transparent 62%, white 65%);
|
|
1995
|
+
animation: feedbackRotate 0.7s linear infinite;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
.feedback-loading-white {
|
|
1999
|
+
background: conic-gradient(from 0deg, rgba(255, 255, 255, 0.5), white);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
.feedback-loading-blue {
|
|
2003
|
+
background: conic-gradient(from 0deg, #004EEB, #eff4ff);
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
@keyframes feedbackRotate {
|
|
2007
|
+
0% { transform: rotate(0deg); }
|
|
2008
|
+
100% { transform: rotate(360deg); }
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
.feedback-error {
|
|
2012
|
+
color: #F04438;
|
|
2013
|
+
font-size: 14px;
|
|
2014
|
+
font-weight: 400;
|
|
2015
|
+
margin-top: 4px;
|
|
2016
|
+
text-transform: capitalize;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
.feedback-form-error {
|
|
2020
|
+
color: #F04438;
|
|
2021
|
+
font-size: 14px;
|
|
2022
|
+
margin-top: 12px;
|
|
2023
|
+
padding: 8px 12px;
|
|
2024
|
+
background: #FEE2E2;
|
|
2025
|
+
border: 1px solid #FECACA;
|
|
2026
|
+
border-radius: 6px;
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
.feedback-modal.theme-dark .feedback-form-error {
|
|
2030
|
+
background: #7F1D1D;
|
|
2031
|
+
border-color: #991B1B;
|
|
2032
|
+
color: #FCA5A5;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
.feedback-form-group.error input,
|
|
2036
|
+
.feedback-form-group.error textarea {
|
|
2037
|
+
border-color: #FDA29B;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
.feedback-form-group.error input:focus,
|
|
2041
|
+
.feedback-form-group.error textarea:focus {
|
|
2042
|
+
border-color: #FDA29B;
|
|
2043
|
+
box-shadow: 0 0 0 1px rgba(16, 24, 40, 0.05), 0 0 0 4px rgba(253, 162, 155, 0.3);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
.feedback-success-notification {
|
|
2047
|
+
position: fixed;
|
|
2048
|
+
top: 20px;
|
|
2049
|
+
right: 20px;
|
|
2050
|
+
z-index: 1000001;
|
|
2051
|
+
background: white;
|
|
2052
|
+
border: 1px solid #D1FAE5;
|
|
2053
|
+
border-radius: 8px;
|
|
2054
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
2055
|
+
animation: slideInRight 0.3s ease-out;
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
.feedback-success-content {
|
|
2059
|
+
display: flex;
|
|
2060
|
+
align-items: center;
|
|
2061
|
+
padding: 12px 16px;
|
|
2062
|
+
gap: 12px;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
.feedback-success-content span {
|
|
2066
|
+
color: #059669;
|
|
2067
|
+
font-weight: 500;
|
|
2068
|
+
font-size: 14px;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
.feedback-success-close {
|
|
2072
|
+
background: none;
|
|
2073
|
+
border: none;
|
|
2074
|
+
color: #6B7280;
|
|
2075
|
+
cursor: pointer;
|
|
2076
|
+
font-size: 18px;
|
|
2077
|
+
padding: 0;
|
|
2078
|
+
width: 20px;
|
|
2079
|
+
height: 20px;
|
|
2080
|
+
display: flex;
|
|
2081
|
+
align-items: center;
|
|
2082
|
+
justify-content: center;
|
|
2083
|
+
transition: all 0.3s ease;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
.feedback-success-close:hover {
|
|
2087
|
+
color: #374151;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
.feedback-success-close:focus-visible {
|
|
2091
|
+
outline: 2px solid #155EEF;
|
|
2092
|
+
outline-offset: 2px;
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
@keyframes slideInRight {
|
|
2096
|
+
from {
|
|
2097
|
+
transform: translateX(100%);
|
|
2098
|
+
opacity: 0;
|
|
2099
|
+
}
|
|
2100
|
+
to {
|
|
2101
|
+
transform: translateX(0);
|
|
2102
|
+
opacity: 1;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
@keyframes fadeIn {
|
|
2107
|
+
from { opacity: 0; }
|
|
2108
|
+
to { opacity: 1; }
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
.feedback-modal {
|
|
2112
|
+
animation: fadeIn 0.2s ease-out;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
.feedback-modal-content {
|
|
2116
|
+
animation: slideInUp 0.3s ease-out;
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
@keyframes slideInUp {
|
|
2120
|
+
from {
|
|
2121
|
+
transform: translateY(20px);
|
|
2122
|
+
opacity: 0;
|
|
2123
|
+
}
|
|
2124
|
+
to {
|
|
2125
|
+
transform: translateY(0);
|
|
2126
|
+
opacity: 1;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
@media (max-width: 640px) {
|
|
2131
|
+
.feedback-modal {
|
|
2132
|
+
padding: 8px;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
.feedback-modal-content {
|
|
2136
|
+
min-width: 280px;
|
|
2137
|
+
max-width: 100%;
|
|
2138
|
+
max-height: 95vh;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
.feedback-form {
|
|
2142
|
+
padding: 16px;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
.feedback-modal-header {
|
|
2146
|
+
padding: 16px;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
.feedback-modal-header h3 {
|
|
2150
|
+
font-size: 15px;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
.feedback-form-actions {
|
|
2154
|
+
flex-direction: column;
|
|
2155
|
+
gap: 8px;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
.feedback-btn {
|
|
2159
|
+
width: 100%;
|
|
2160
|
+
height: 40px;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
.feedback-widget-button {
|
|
2164
|
+
bottom: 16px;
|
|
2165
|
+
right: 16px;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
.feedback-widget-button.position-bottom-left {
|
|
2169
|
+
left: 16px;
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
.feedback-trigger-btn {
|
|
2173
|
+
padding: 10px 16px;
|
|
2174
|
+
font-size: 13px;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
.feedback-success-notification {
|
|
2178
|
+
top: 8px;
|
|
2179
|
+
right: 8px;
|
|
2180
|
+
left: 8px;
|
|
2181
|
+
max-width: none;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
.feedback-form-group input {
|
|
2185
|
+
height: 40px;
|
|
2186
|
+
padding: 2px 12px;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
.feedback-form-group textarea {
|
|
2190
|
+
min-height: 80px;
|
|
2191
|
+
padding: 2px 12px;
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2196
|
+
.feedback-trigger-btn,
|
|
2197
|
+
.feedback-btn,
|
|
2198
|
+
.feedback-modal,
|
|
2199
|
+
.feedback-modal-content,
|
|
2200
|
+
.feedback-success-notification,
|
|
2201
|
+
.feedback-loading {
|
|
2202
|
+
transition: none;
|
|
2203
|
+
animation: none;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
.feedback-trigger-btn:hover {
|
|
2207
|
+
transform: none;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
@media print {
|
|
2212
|
+
.feedback-widget,
|
|
2213
|
+
.feedback-modal,
|
|
2214
|
+
.feedback-success-notification {
|
|
2215
|
+
display: none !important;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
`;
|
|
2219
|
+
|
|
2220
|
+
function injectStyles() {
|
|
2221
|
+
console.log('injectStyles called');
|
|
2222
|
+
console.log('document exists:', typeof document !== 'undefined');
|
|
2223
|
+
console.log('CSS_STYLES exists:', true);
|
|
2224
|
+
|
|
2225
|
+
if (
|
|
2226
|
+
typeof document !== 'undefined' &&
|
|
2227
|
+
!document.querySelector('#feedback-sdk-styles')
|
|
2228
|
+
) {
|
|
2229
|
+
console.log('Injecting CSS...');
|
|
2230
|
+
const style = document.createElement('style');
|
|
2231
|
+
style.id = 'feedback-sdk-styles';
|
|
2232
|
+
style.textContent = CSS_STYLES;
|
|
2233
|
+
document.head.appendChild(style);
|
|
2234
|
+
console.log(
|
|
2235
|
+
'CSS injected, style element created:',
|
|
2236
|
+
!!document.querySelector('#feedback-sdk-styles')
|
|
2237
|
+
);
|
|
2238
|
+
} else {
|
|
2239
|
+
console.log('CSS already exists or document not ready');
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
function autoInit() {
|
|
2244
|
+
if (typeof window !== 'undefined' && window.FeedbackSDKConfig) {
|
|
2245
|
+
injectStyles();
|
|
2246
|
+
|
|
2247
|
+
const config = { ...window.FeedbackSDKConfig };
|
|
2248
|
+
|
|
2249
|
+
if (!config.userContext) {
|
|
2250
|
+
config.userContext = getUserContextFromEnvironment();
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
const sdk = new FeedbackSDK(config);
|
|
2254
|
+
|
|
2255
|
+
sdk
|
|
2256
|
+
.init()
|
|
2257
|
+
.then((initData) => {
|
|
2258
|
+
window.FeedbackSDK.instance = sdk;
|
|
2259
|
+
|
|
2260
|
+
if (window.FeedbackSDKConfig.autoCreate) {
|
|
2261
|
+
const widgets = Array.isArray(window.FeedbackSDKConfig.autoCreate)
|
|
2262
|
+
? window.FeedbackSDKConfig.autoCreate
|
|
2263
|
+
: [window.FeedbackSDKConfig.autoCreate];
|
|
2264
|
+
|
|
2265
|
+
widgets.forEach((widgetConfig) => {
|
|
2266
|
+
try {
|
|
2267
|
+
const widget = sdk.createWidget(
|
|
2268
|
+
widgetConfig.type || 'button',
|
|
2269
|
+
widgetConfig
|
|
2270
|
+
);
|
|
2271
|
+
widget.mount(widgetConfig.container);
|
|
2272
|
+
} catch (error) {
|
|
2273
|
+
console.error('[FeedbackSDK] Failed to create widget:', error);
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
if (typeof CustomEvent !== 'undefined') {
|
|
2279
|
+
const event = new CustomEvent('FeedbackSDKReady', {
|
|
2280
|
+
detail: {
|
|
2281
|
+
sdk,
|
|
2282
|
+
config: config,
|
|
2283
|
+
initData: initData,
|
|
2284
|
+
},
|
|
2285
|
+
});
|
|
2286
|
+
window.dispatchEvent(event);
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
console.log(
|
|
2290
|
+
'[FeedbackSDK] Successfully initialized with session:',
|
|
2291
|
+
initData.sessionToken ? 'Yes' : 'No'
|
|
2292
|
+
);
|
|
2293
|
+
})
|
|
2294
|
+
.catch((error) => {
|
|
2295
|
+
console.error('[FeedbackSDK] Auto-initialization failed:', error);
|
|
2296
|
+
|
|
2297
|
+
if (typeof CustomEvent !== 'undefined') {
|
|
2298
|
+
const event = new CustomEvent('FeedbackSDKError', {
|
|
2299
|
+
detail: {
|
|
2300
|
+
error,
|
|
2301
|
+
config: config,
|
|
2302
|
+
phase: 'initialization',
|
|
2303
|
+
},
|
|
2304
|
+
});
|
|
2305
|
+
window.dispatchEvent(event);
|
|
2306
|
+
}
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
function getUserContextFromEnvironment() {
|
|
2312
|
+
if (typeof window === 'undefined') return null;
|
|
2313
|
+
|
|
2314
|
+
if (window.FeedbackSDKUserContext) {
|
|
2315
|
+
return window.FeedbackSDKUserContext;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
const authSources = [
|
|
2319
|
+
() => window.auth0?.user,
|
|
2320
|
+
() => window.firebase?.auth()?.currentUser,
|
|
2321
|
+
() => window.amplify?.Auth?.currentAuthenticatedUser(),
|
|
2322
|
+
|
|
2323
|
+
() => window.currentUser,
|
|
2324
|
+
() => window.user,
|
|
2325
|
+
() => window.userData,
|
|
2326
|
+
|
|
2327
|
+
() => window.app?.user,
|
|
2328
|
+
() => window.store?.getState?.()?.user,
|
|
2329
|
+
() => window.App?.currentUser,
|
|
2330
|
+
];
|
|
2331
|
+
|
|
2332
|
+
for (const getAuth of authSources) {
|
|
2333
|
+
try {
|
|
2334
|
+
const authData = getAuth();
|
|
2335
|
+
if (authData) {
|
|
2336
|
+
const userContext = FeedbackSDK.extractUserContextFromAuth(authData);
|
|
2337
|
+
if (userContext && (userContext.user_id || userContext.email)) {
|
|
2338
|
+
console.log(
|
|
2339
|
+
'[FeedbackSDK] Auto-detected user context from',
|
|
2340
|
+
getAuth.name || 'unknown source'
|
|
2341
|
+
);
|
|
2342
|
+
return userContext;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
} catch (error) {
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
try {
|
|
2351
|
+
const storedAuth =
|
|
2352
|
+
localStorage.getItem('auth') ||
|
|
2353
|
+
localStorage.getItem('user') ||
|
|
2354
|
+
localStorage.getItem('session');
|
|
2355
|
+
if (storedAuth) {
|
|
2356
|
+
const authData = JSON.parse(storedAuth);
|
|
2357
|
+
const userContext = FeedbackSDK.extractUserContextFromAuth(authData);
|
|
2358
|
+
if (userContext && (userContext.user_id || userContext.email)) {
|
|
2359
|
+
console.log(
|
|
2360
|
+
'[FeedbackSDK] Auto-detected user context from localStorage'
|
|
2361
|
+
);
|
|
2362
|
+
return userContext;
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
} catch (error) {
|
|
2366
|
+
// Continue
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
console.warn(
|
|
2370
|
+
'[FeedbackSDK] No user context found. Widget initialization may require manual user context setting.'
|
|
2371
|
+
);
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
function handleDOMReady() {
|
|
2376
|
+
if (typeof document !== 'undefined') {
|
|
2377
|
+
if (document.readyState === 'loading') {
|
|
2378
|
+
document.addEventListener('DOMContentLoaded', autoInit);
|
|
2379
|
+
} else {
|
|
2380
|
+
setTimeout(autoInit, 0);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
const FeedbackSDKExport = {
|
|
2386
|
+
FeedbackSDK,
|
|
2387
|
+
BaseWidget,
|
|
2388
|
+
ButtonWidget,
|
|
2389
|
+
TabWidget,
|
|
2390
|
+
InlineWidget,
|
|
2391
|
+
WidgetFactory,
|
|
2392
|
+
EventBus,
|
|
2393
|
+
APIService,
|
|
2394
|
+
SDKError,
|
|
2395
|
+
APIError,
|
|
2396
|
+
WidgetError,
|
|
2397
|
+
ConfigError,
|
|
2398
|
+
ValidationError,
|
|
2399
|
+
helpers,
|
|
2400
|
+
create: (config) => {
|
|
2401
|
+
injectStyles();
|
|
2402
|
+
return new FeedbackSDK(config);
|
|
2403
|
+
},
|
|
2404
|
+
version: '1.0.0',
|
|
2405
|
+
instance: null,
|
|
2406
|
+
|
|
2407
|
+
isReady: () => Boolean(FeedbackSDKExport.instance),
|
|
2408
|
+
getInstance: () => FeedbackSDKExport.instance,
|
|
2409
|
+
|
|
2410
|
+
setUserContext: (userContext) => {
|
|
2411
|
+
if (FeedbackSDKExport.instance) {
|
|
2412
|
+
FeedbackSDKExport.instance.setUserContext(userContext);
|
|
2413
|
+
} else {
|
|
2414
|
+
if (typeof window !== 'undefined') {
|
|
2415
|
+
window.FeedbackSDKUserContext = userContext;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
},
|
|
2419
|
+
|
|
2420
|
+
initWithUser: async (config, userContext) => {
|
|
2421
|
+
injectStyles();
|
|
2422
|
+
const fullConfig = { ...config, userContext };
|
|
2423
|
+
const sdk = new FeedbackSDK(fullConfig);
|
|
2424
|
+
await sdk.init();
|
|
2425
|
+
|
|
2426
|
+
if (typeof window !== 'undefined') {
|
|
2427
|
+
window.FeedbackSDK.instance = sdk;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
return sdk;
|
|
2431
|
+
},
|
|
2432
|
+
|
|
2433
|
+
onReady: (callback) => {
|
|
2434
|
+
if (typeof window !== 'undefined') {
|
|
2435
|
+
if (FeedbackSDKExport.isReady()) {
|
|
2436
|
+
callback(FeedbackSDKExport.instance);
|
|
2437
|
+
} else {
|
|
2438
|
+
window.addEventListener(
|
|
2439
|
+
'FeedbackSDKReady',
|
|
2440
|
+
(event) => {
|
|
2441
|
+
callback(event.detail.sdk, event.detail);
|
|
2442
|
+
},
|
|
2443
|
+
{ once: true }
|
|
2444
|
+
);
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
|
|
2449
|
+
onError: (callback) => {
|
|
2450
|
+
if (typeof window !== 'undefined') {
|
|
2451
|
+
window.addEventListener('FeedbackSDKError', (event) => {
|
|
2452
|
+
callback(event.detail.error, event.detail);
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
},
|
|
2456
|
+
|
|
2457
|
+
extractUserContext: FeedbackSDK.extractUserContextFromAuth,
|
|
2458
|
+
};
|
|
2459
|
+
|
|
2460
|
+
if (typeof window !== 'undefined') {
|
|
2461
|
+
window.FeedbackSDK = FeedbackSDKExport;
|
|
2462
|
+
handleDOMReady();
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
exports.APIError = APIError;
|
|
2466
|
+
exports.APIService = APIService;
|
|
2467
|
+
exports.BaseWidget = BaseWidget;
|
|
2468
|
+
exports.ButtonWidget = ButtonWidget;
|
|
2469
|
+
exports.ConfigError = ConfigError;
|
|
2470
|
+
exports.EventBus = EventBus;
|
|
2471
|
+
exports.FeedbackSDK = FeedbackSDK;
|
|
2472
|
+
exports.InlineWidget = InlineWidget;
|
|
2473
|
+
exports.SDKError = SDKError;
|
|
2474
|
+
exports.TabWidget = TabWidget;
|
|
2475
|
+
exports.ValidationError = ValidationError;
|
|
2476
|
+
exports.WidgetError = WidgetError;
|
|
2477
|
+
exports.WidgetFactory = WidgetFactory;
|
|
2478
|
+
exports.default = FeedbackSDKExport;
|
|
2479
|
+
exports.helpers = helpers;
|
|
2480
|
+
|
|
2481
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2482
|
+
|
|
2483
|
+
}));
|