@product7/feedback-sdk 1.1.0 → 1.1.4

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.
@@ -1,18 +1,44 @@
1
1
  import { APIError } from '../utils/errors.js';
2
2
 
3
+ const MOCK_CONFIG = {
4
+ primaryColor: '#155EEF',
5
+ backgroundColor: '#ffffff',
6
+ textColor: '#1F2937',
7
+ boardId: 'feature-requests',
8
+ size: 'medium',
9
+ displayMode: 'modal',
10
+ };
11
+
12
+ // Environment URLs
13
+ const ENV_URLS = {
14
+ production: {
15
+ base: 'https://api.product7.io/api/v1',
16
+ withWorkspace: (workspace) => `https://${workspace}.api.product7.io/api/v1`,
17
+ },
18
+ staging: {
19
+ base: 'https://staging.api.product7.io/api/v1',
20
+ withWorkspace: (workspace) => `https://${workspace}.staging.api.product7.io/api/v1`,
21
+ },
22
+ };
23
+
3
24
  export class APIService {
4
25
  constructor(config = {}) {
5
26
  this.workspace = config.workspace;
6
27
  this.sessionToken = null;
7
28
  this.sessionExpiry = null;
8
29
  this.userContext = config.userContext || null;
30
+ this.mock = config.mock || false;
31
+ this.env = config.env || 'production'; // 'production' or 'staging'
9
32
 
10
33
  if (config.apiUrl) {
34
+ // Custom API URL takes precedence
11
35
  this.baseURL = config.apiUrl;
12
- } else if (this.workspace) {
13
- this.baseURL = `https://${this.workspace}.staging.api.product7.io/api/v1`;
14
36
  } else {
15
- this.baseURL = 'https://staging.api.product7.io/api/v1';
37
+ // Use environment-based URL
38
+ const envConfig = ENV_URLS[this.env] || ENV_URLS.production;
39
+ this.baseURL = this.workspace
40
+ ? envConfig.withWorkspace(this.workspace)
41
+ : envConfig.base;
16
42
  }
17
43
 
18
44
  this._loadStoredSession();
@@ -32,6 +58,18 @@ export class APIService {
32
58
  throw new APIError(400, error);
33
59
  }
34
60
 
61
+ // Mock mode - return fake session
62
+ if (this.mock) {
63
+ this.sessionToken = 'mock_session_' + Date.now();
64
+ this.sessionExpiry = new Date(Date.now() + 3600 * 1000);
65
+ this._storeSession();
66
+ return {
67
+ sessionToken: this.sessionToken,
68
+ config: MOCK_CONFIG,
69
+ expiresIn: 3600,
70
+ };
71
+ }
72
+
35
73
  const payload = {
36
74
  workspace: this.workspace,
37
75
  user: this.userContext,
@@ -73,6 +111,20 @@ export class APIService {
73
111
  throw new APIError(401, 'No valid session token available');
74
112
  }
75
113
 
114
+ // Mock mode - simulate success
115
+ if (this.mock) {
116
+ await new Promise(resolve => setTimeout(resolve, 500));
117
+ return {
118
+ success: true,
119
+ data: {
120
+ id: 'mock_post_' + Date.now(),
121
+ title: feedbackData.title,
122
+ content: feedbackData.content,
123
+ },
124
+ message: 'Feedback submitted successfully!',
125
+ };
126
+ }
127
+
76
128
  const payload = {
77
129
  board: feedbackData.board_id || feedbackData.board || feedbackData.boardId,
78
130
  title: feedbackData.title,
@@ -15,6 +15,8 @@ export class FeedbackSDK {
15
15
  apiUrl: this.config.apiUrl,
16
16
  workspace: this.config.workspace,
17
17
  userContext: this.config.userContext,
18
+ mock: this.config.mock,
19
+ env: this.config.env,
18
20
  });
19
21
 
20
22
  this._bindMethods();
@@ -28,8 +30,9 @@ export class FeedbackSDK {
28
30
  try {
29
31
  const initData = await this.apiService.init(this.config.userContext);
30
32
 
33
+ // Merge backend config as base, local config overrides
31
34
  if (initData.config) {
32
- this.config = deepMerge(this.config, initData.config);
35
+ this.config = deepMerge(initData.config, this.config);
33
36
  }
34
37
 
35
38
  this.initialized = true;
@@ -78,6 +81,44 @@ export class FeedbackSDK {
78
81
  return this.widgets.get(id);
79
82
  }
80
83
 
84
+ /**
85
+ * Show a survey widget
86
+ * @param {Object} options - Survey options
87
+ * @param {string} options.surveyType - Type of survey: 'nps', 'csat', 'ces', 'custom'
88
+ * @param {string} options.position - Position: 'bottom-right', 'bottom-left', 'center', 'bottom'
89
+ * @param {string} options.theme - Theme: 'light', 'dark'
90
+ * @param {string} options.title - Custom title
91
+ * @param {string} options.description - Custom description
92
+ * @param {string} options.lowLabel - Low end label
93
+ * @param {string} options.highLabel - High end label
94
+ * @param {Function} options.onSubmit - Callback when survey is submitted
95
+ * @param {Function} options.onDismiss - Callback when survey is dismissed
96
+ * @returns {SurveyWidget} The survey widget instance
97
+ */
98
+ showSurvey(options = {}) {
99
+ if (!this.initialized) {
100
+ throw new SDKError('SDK must be initialized before showing surveys. Call init() first.');
101
+ }
102
+
103
+ const surveyWidget = this.createWidget('survey', {
104
+ surveyType: options.surveyType || options.type || 'nps',
105
+ position: options.position || 'bottom-right',
106
+ theme: options.theme || this.config.theme || 'light',
107
+ title: options.title,
108
+ description: options.description,
109
+ lowLabel: options.lowLabel,
110
+ highLabel: options.highLabel,
111
+ customQuestions: options.customQuestions,
112
+ onSubmit: options.onSubmit,
113
+ onDismiss: options.onDismiss,
114
+ });
115
+
116
+ surveyWidget.mount();
117
+ surveyWidget.show();
118
+
119
+ return surveyWidget;
120
+ }
121
+
81
122
  getAllWidgets() {
82
123
  return Array.from(this.widgets.values());
83
124
  }
@@ -176,6 +217,8 @@ export class FeedbackSDK {
176
217
  boardId: 'general',
177
218
  autoShow: true,
178
219
  debug: false,
220
+ mock: false,
221
+ env: 'production', // 'production' or 'staging'
179
222
  };
180
223
 
181
224
  const mergedConfig = deepMerge(deepMerge(defaultConfig, existingConfig), newConfig);
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import * as helpers from './utils/helpers.js';
13
13
  import { BaseWidget } from './widgets/BaseWidget.js';
14
14
  import { ButtonWidget } from './widgets/ButtonWidget.js';
15
15
  import { InlineWidget } from './widgets/InlineWidget.js';
16
+ import { SurveyWidget } from './widgets/SurveyWidget.js';
16
17
  import { TabWidget } from './widgets/TabWidget.js';
17
18
  import { WidgetFactory } from './widgets/WidgetFactory.js';
18
19
 
@@ -91,6 +92,7 @@ const FeedbackSDKExport = {
91
92
  ButtonWidget,
92
93
  TabWidget,
93
94
  InlineWidget,
95
+ SurveyWidget,
94
96
  WidgetFactory,
95
97
  EventBus,
96
98
  APIService,
@@ -178,6 +180,7 @@ export {
178
180
  helpers,
179
181
  InlineWidget,
180
182
  SDKError,
183
+ SurveyWidget,
181
184
  TabWidget,
182
185
  ValidationError,
183
186
  WidgetError,
@@ -38,6 +38,24 @@ export const CSS_STYLES = `
38
38
  left: 20px;
39
39
  }
40
40
 
41
+ .feedback-widget-button.position-bottom-center {
42
+ bottom: 20px;
43
+ left: 50%;
44
+ transform: translateX(-50%);
45
+ }
46
+
47
+ .feedback-widget-button.position-top-center {
48
+ top: 20px;
49
+ left: 50%;
50
+ transform: translateX(-50%);
51
+ }
52
+
53
+ .feedback-widget-button.position-center {
54
+ top: 50%;
55
+ left: 50%;
56
+ transform: translate(-50%, -50%);
57
+ }
58
+
41
59
  /* Circular button design with white bg and blue border */
42
60
  .feedback-trigger-btn {
43
61
  width: 56px;
@@ -81,6 +99,99 @@ export const CSS_STYLES = `
81
99
  color: #ffffff;
82
100
  }
83
101
 
102
+ /* Loading Modal */
103
+ .feedback-loading-modal {
104
+ position: fixed;
105
+ top: 50%;
106
+ left: 50%;
107
+ transform: translate(-50%, -50%) scale(0.9);
108
+ z-index: 1000001;
109
+ opacity: 0;
110
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
111
+ }
112
+
113
+ .feedback-loading-modal.show {
114
+ opacity: 1;
115
+ transform: translate(-50%, -50%) scale(1);
116
+ }
117
+
118
+ .feedback-loading-spinner {
119
+ width: 48px;
120
+ height: 48px;
121
+ border: 4px solid rgba(255, 255, 255, 0.3);
122
+ border-top-color: #155EEF;
123
+ border-radius: 50%;
124
+ animation: spin 0.8s linear infinite;
125
+ }
126
+
127
+ .theme-dark .feedback-loading-spinner {
128
+ border-color: rgba(255, 255, 255, 0.2);
129
+ border-top-color: #155EEF;
130
+ }
131
+
132
+ @keyframes spin {
133
+ to { transform: rotate(360deg); }
134
+ }
135
+
136
+ /* Modal Styles (centered) */
137
+ .feedback-modal {
138
+ position: fixed;
139
+ top: 50%;
140
+ left: 50%;
141
+ transform: translate(-50%, -50%) scale(0.9);
142
+ width: 480px;
143
+ max-width: 90vw;
144
+ max-height: 85vh;
145
+ z-index: 1000000;
146
+ opacity: 0;
147
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
148
+ font-family: inherit;
149
+ }
150
+
151
+ .feedback-modal.open {
152
+ opacity: 1;
153
+ transform: translate(-50%, -50%) scale(1);
154
+ }
155
+
156
+ .feedback-modal .feedback-panel-content {
157
+ max-height: 85vh;
158
+ overflow-y: auto;
159
+ }
160
+
161
+ /* Size variants */
162
+ .feedback-modal.size-small {
163
+ width: 360px;
164
+ }
165
+
166
+ .feedback-modal.size-medium {
167
+ width: 480px;
168
+ }
169
+
170
+ .feedback-modal.size-large {
171
+ width: 600px;
172
+ }
173
+
174
+ .feedback-panel.size-small {
175
+ width: 320px;
176
+ }
177
+
178
+ .feedback-panel.size-medium {
179
+ width: 420px;
180
+ }
181
+
182
+ .feedback-panel.size-large {
183
+ width: 520px;
184
+ }
185
+
186
+ /* Adjust textarea height for sizes */
187
+ .size-small .feedback-form-group textarea {
188
+ min-height: 120px;
189
+ }
190
+
191
+ .size-large .feedback-form-group textarea {
192
+ min-height: 280px;
193
+ }
194
+
84
195
  /* Side Panel Styles */
85
196
  .feedback-panel {
86
197
  position: fixed;
@@ -104,7 +215,7 @@ export const CSS_STYLES = `
104
215
  left: 0;
105
216
  right: 0;
106
217
  bottom: 0;
107
- background: rgba(0, 0, 0, 0.1);
218
+ background: rgba(0, 0, 0, 0.5);
108
219
  opacity: 0;
109
220
  transition: opacity 0.3s ease;
110
221
  pointer-events: none;
@@ -117,43 +228,31 @@ export const CSS_STYLES = `
117
228
  }
118
229
 
119
230
  .feedback-panel-content {
120
- background: white;
231
+ background: var(--bg-color, #ffffff);
232
+ color: var(--text-color, #1F2937);
121
233
  height: 100%;
122
234
  display: flex;
123
235
  flex-direction: column;
124
236
  border-radius: 16px;
125
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
237
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
126
238
  0 10px 10px -5px rgba(0, 0, 0, 0.04),
127
239
  0 0 0 1px rgba(0, 0, 0, 0.05);
128
240
  }
129
241
 
130
- .feedback-panel.theme-dark .feedback-panel-content {
131
- background: #1F2937;
132
- color: white;
133
- }
134
-
135
242
  .feedback-panel-header {
136
243
  display: flex;
137
244
  align-items: center;
138
245
  justify-content: space-between;
139
- padding: 24px;
140
- border-bottom: 1px solid #E5E7EB;
246
+ padding: 16px 20px;
247
+ border-bottom: 1px solid rgba(128, 128, 128, 0.2);
141
248
  flex-shrink: 0;
142
249
  }
143
250
 
144
- .feedback-panel.theme-dark .feedback-panel-header {
145
- border-bottom-color: #374151;
146
- }
147
-
148
251
  .feedback-panel-header h3 {
149
252
  margin: 0;
150
253
  font-size: 18px;
151
254
  font-weight: 600;
152
- color: #111827;
153
- }
154
-
155
- .feedback-panel.theme-dark .feedback-panel-header h3 {
156
- color: white;
255
+ color: var(--text-color, #111827);
157
256
  }
158
257
 
159
258
  .feedback-panel-close {
@@ -161,7 +260,8 @@ export const CSS_STYLES = `
161
260
  border: none;
162
261
  font-size: 24px;
163
262
  cursor: pointer;
164
- color: #6B7280;
263
+ color: var(--text-color, #6B7280);
264
+ opacity: 0.6;
165
265
  padding: 4px;
166
266
  width: 32px;
167
267
  height: 32px;
@@ -173,28 +273,19 @@ export const CSS_STYLES = `
173
273
  }
174
274
 
175
275
  .feedback-panel-close:hover {
176
- background: #F3F4F6;
177
- color: #111827;
276
+ opacity: 1;
277
+ background: rgba(128, 128, 128, 0.1);
178
278
  }
179
279
 
180
280
  .feedback-panel-close:focus-visible {
181
- outline: 2px solid #155EEF;
281
+ outline: 2px solid var(--primary-color, #155EEF);
182
282
  outline-offset: 2px;
183
283
  }
184
284
 
185
- .feedback-panel.theme-dark .feedback-panel-close {
186
- color: #9CA3AF;
187
- }
188
-
189
- .feedback-panel.theme-dark .feedback-panel-close:hover {
190
- background: #374151;
191
- color: white;
192
- }
193
-
194
285
  .feedback-panel-body {
195
286
  flex: 1;
196
287
  overflow-y: auto;
197
- padding: 24px;
288
+ padding: 16px 20px;
198
289
  }
199
290
 
200
291
  .feedback-form {
@@ -218,23 +309,21 @@ export const CSS_STYLES = `
218
309
  font-size: 14px;
219
310
  font-weight: 500;
220
311
  line-height: 1.25;
221
- color: #374151;
222
- }
223
-
224
- .feedback-panel.theme-dark .feedback-form-group label {
225
- color: #D1D5DB;
312
+ color: var(--text-color, #374151);
313
+ opacity: 0.8;
226
314
  }
227
315
 
228
316
  .feedback-form-group input {
229
317
  height: 44px;
230
318
  width: 100%;
231
319
  border-radius: 8px;
232
- border: 1px solid #D1D5DB;
320
+ border: 1px solid rgba(128, 128, 128, 0.3);
321
+ background: rgba(128, 128, 128, 0.05);
233
322
  padding: 10px 14px;
234
323
  font-size: 15px;
235
324
  font-weight: 400;
236
325
  line-height: 1.5;
237
- color: #1F2937;
326
+ color: var(--text-color, #1F2937);
238
327
  font-family: inherit;
239
328
  outline: none;
240
329
  transition: all 0.2s ease;
@@ -242,11 +331,12 @@ export const CSS_STYLES = `
242
331
 
243
332
  .feedback-form-group input::placeholder {
244
333
  font-size: 15px;
245
- color: #9CA3AF;
334
+ color: var(--text-color, #9CA3AF);
335
+ opacity: 0.5;
246
336
  }
247
337
 
248
338
  .feedback-form-group input:focus {
249
- border-color: #155EEF;
339
+ border-color: var(--primary-color, #155EEF);
250
340
  box-shadow: 0 0 0 3px rgba(21, 94, 239, 0.1);
251
341
  }
252
342
 
@@ -259,12 +349,13 @@ export const CSS_STYLES = `
259
349
  width: 100%;
260
350
  resize: vertical;
261
351
  border-radius: 8px;
262
- border: 1px solid #D1D5DB;
352
+ border: 1px solid rgba(128, 128, 128, 0.3);
353
+ background: rgba(128, 128, 128, 0.05);
263
354
  padding: 10px 14px;
264
355
  font-size: 15px;
265
356
  font-weight: 400;
266
357
  line-height: 1.5;
267
- color: #1F2937;
358
+ color: var(--text-color, #1F2937);
268
359
  font-family: inherit;
269
360
  outline: none;
270
361
  transition: all 0.2s ease;
@@ -272,11 +363,12 @@ export const CSS_STYLES = `
272
363
 
273
364
  .feedback-form-group textarea::placeholder {
274
365
  font-size: 15px;
275
- color: #9CA3AF;
366
+ color: var(--text-color, #9CA3AF);
367
+ opacity: 0.5;
276
368
  }
277
369
 
278
370
  .feedback-form-group textarea:focus {
279
- border-color: #155EEF;
371
+ border-color: var(--primary-color, #155EEF);
280
372
  box-shadow: 0 0 0 3px rgba(21, 94, 239, 0.1);
281
373
  }
282
374
 
@@ -284,18 +376,6 @@ export const CSS_STYLES = `
284
376
  outline: none;
285
377
  }
286
378
 
287
- .feedback-panel.theme-dark .feedback-form-group input,
288
- .feedback-panel.theme-dark .feedback-form-group textarea {
289
- background: #374151;
290
- border-color: #4B5563;
291
- color: white;
292
- }
293
-
294
- .feedback-panel.theme-dark .feedback-form-group input::placeholder,
295
- .feedback-panel.theme-dark .feedback-form-group textarea::placeholder {
296
- color: #6B7280;
297
- }
298
-
299
379
  .feedback-btn {
300
380
  position: relative;
301
381
  display: inline-flex;
@@ -324,38 +404,29 @@ export const CSS_STYLES = `
324
404
  }
325
405
 
326
406
  .feedback-btn-submit {
327
- background: #155EEF;
407
+ background: var(--primary-color, #155EEF);
328
408
  color: white;
329
409
  width: 100%;
330
410
  }
331
411
 
332
412
  .feedback-btn-submit:hover:not(:disabled) {
333
- background: #1A56DB;
413
+ background: var(--primary-color, #155EEF);
414
+ filter: brightness(0.9);
334
415
  }
335
416
 
336
417
  .feedback-btn-submit:active:not(:disabled) {
337
- background: #1E429F;
418
+ background: var(--primary-color, #155EEF);
419
+ filter: brightness(0.8);
338
420
  }
339
421
 
340
422
  .feedback-btn-cancel {
341
423
  background: transparent;
342
- color: #6B7280;
343
- border: 1px solid #D1D5DB;
424
+ color: var(--text-color, #6B7280);
425
+ border: 1px solid rgba(128, 128, 128, 0.3);
344
426
  }
345
427
 
346
428
  .feedback-btn-cancel:hover:not(:disabled) {
347
- background: #F9FAFB;
348
- border-color: #9CA3AF;
349
- color: #374151;
350
- }
351
-
352
- .feedback-panel.theme-dark .feedback-btn-cancel {
353
- color: #D1D5DB;
354
- border-color: #4B5563;
355
- }
356
-
357
- .feedback-panel.theme-dark .feedback-btn-cancel:hover:not(:disabled) {
358
- background: #374151;
429
+ background: rgba(128, 128, 128, 0.1);
359
430
  }
360
431
 
361
432
  .feedback-form-actions {
@@ -562,4 +633,68 @@ export const CSS_STYLES = `
562
633
  display: none !important;
563
634
  }
564
635
  }
636
+
637
+ /* Survey Widget Styles */
638
+ .feedback-survey-backdrop {
639
+ position: fixed;
640
+ top: 0;
641
+ left: 0;
642
+ right: 0;
643
+ bottom: 0;
644
+ background: rgba(0, 0, 0, 0.5);
645
+ z-index: 9999;
646
+ opacity: 0;
647
+ transition: opacity 0.3s ease;
648
+ }
649
+
650
+ .feedback-survey-backdrop.show {
651
+ opacity: 1;
652
+ }
653
+
654
+ .feedback-survey {
655
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
656
+ }
657
+
658
+ .feedback-survey-csat-btn:hover {
659
+ transform: scale(1.1) !important;
660
+ }
661
+
662
+ .feedback-survey-nps-btn:hover,
663
+ .feedback-survey-ces-btn:hover,
664
+ .feedback-survey-freq-btn:hover {
665
+ border-color: #007aff !important;
666
+ }
667
+
668
+ @media (max-width: 768px) {
669
+ .feedback-survey {
670
+ left: 16px !important;
671
+ right: 16px !important;
672
+ max-width: none !important;
673
+ min-width: auto !important;
674
+ }
675
+
676
+ .feedback-survey.feedback-survey-center {
677
+ width: calc(100% - 32px) !important;
678
+ }
679
+
680
+ .feedback-survey-nps {
681
+ flex-wrap: wrap;
682
+ justify-content: center !important;
683
+ }
684
+
685
+ .feedback-survey-nps-btn {
686
+ width: 32px !important;
687
+ height: 32px !important;
688
+ font-size: 11px !important;
689
+ }
690
+
691
+ .feedback-survey-ces {
692
+ flex-direction: column !important;
693
+ }
694
+
695
+ .feedback-survey-ces-btn {
696
+ flex: none !important;
697
+ width: 100% !important;
698
+ }
699
+ }
565
700
  `;