@product7/feedback-sdk 1.2.4 → 1.2.6

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.
@@ -0,0 +1,619 @@
1
+ import { BaseWidget } from './BaseWidget.js';
2
+
3
+ export class ChangelogWidget extends BaseWidget {
4
+ constructor(options) {
5
+ super({ ...options, type: 'changelog' });
6
+ this.changelogs = [];
7
+ this.isLoading = false;
8
+ this.modalElement = null;
9
+ this.sidebarElement = null;
10
+ this.currentIndex = 0;
11
+ }
12
+
13
+ _render() {
14
+ const trigger = document.createElement('div');
15
+ trigger.className = `feedback-widget changelog-widget theme-${this.options.theme} position-${this.options.position}`;
16
+ trigger.innerHTML = `
17
+ <button class="changelog-trigger-btn" type="button" aria-label="View Updates">
18
+ <svg class="changelog-icon" width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
19
+ <path d="M5.8 21L7.4 14L2 9.2L9.2 8.6L12 2L14.8 8.6L22 9.2L16.6 14L18.2 21L12 17.3L5.8 21Z"/>
20
+ </svg>
21
+ <span class="changelog-text">${this.options.triggerText || "What's New"}</span>
22
+ <span class="changelog-confetti-emoji">🎉</span>
23
+ ${this.options.showBadge ? '<span class="changelog-badge"></span>' : ''}
24
+ </button>
25
+ `;
26
+
27
+ if (this.options.customStyles) {
28
+ Object.assign(trigger.style, this.options.customStyles);
29
+ }
30
+
31
+ return trigger;
32
+ }
33
+
34
+ _attachEvents() {
35
+ const button = this.element.querySelector('.changelog-trigger-btn');
36
+
37
+ button.addEventListener('click', () => {
38
+ this.openSidebar();
39
+ });
40
+
41
+ button.addEventListener('mouseenter', () => {
42
+ button.style.transform = 'translateY(-2px)';
43
+ });
44
+
45
+ button.addEventListener('mouseleave', () => {
46
+ button.style.transform = 'translateY(0)';
47
+ });
48
+ }
49
+
50
+ // ==================== POPUP MODAL ====================
51
+
52
+ async openModal() {
53
+ if (this.modalElement) return;
54
+
55
+ this.state.isOpen = true;
56
+ this._createModal();
57
+
58
+ // Load changelogs if not already loaded
59
+ if (this.changelogs.length === 0) {
60
+ await this._loadChangelogs();
61
+ }
62
+
63
+ this.currentIndex = 0;
64
+ this._renderCurrentChangelog();
65
+
66
+ // Prevent body scroll
67
+ document.body.style.overflow = 'hidden';
68
+
69
+ requestAnimationFrame(() => {
70
+ if (this.modalElement) {
71
+ this.modalElement.classList.add('open');
72
+ }
73
+ if (this.backdropElement) {
74
+ this.backdropElement.classList.add('show');
75
+ }
76
+ });
77
+ }
78
+
79
+ closeModal() {
80
+ // Restore body scroll
81
+ document.body.style.overflow = '';
82
+
83
+ if (this.modalElement) {
84
+ this.modalElement.classList.remove('open');
85
+ }
86
+ if (this.backdropElement) {
87
+ this.backdropElement.classList.remove('show');
88
+ }
89
+
90
+ setTimeout(() => {
91
+ this.state.isOpen = false;
92
+ if (this.modalElement && this.modalElement.parentNode) {
93
+ this.modalElement.parentNode.removeChild(this.modalElement);
94
+ this.modalElement = null;
95
+ }
96
+ if (this.backdropElement && this.backdropElement.parentNode) {
97
+ this.backdropElement.parentNode.removeChild(this.backdropElement);
98
+ this.backdropElement = null;
99
+ }
100
+ }, 300);
101
+ }
102
+
103
+ _createModal() {
104
+ // Create backdrop
105
+ this.backdropElement = document.createElement('div');
106
+ this.backdropElement.className = 'changelog-modal-backdrop';
107
+ document.body.appendChild(this.backdropElement);
108
+ this.backdropElement.addEventListener('click', () => this.closeModal());
109
+
110
+ // Create modal
111
+ this.modalElement = document.createElement('div');
112
+ this.modalElement.className = `changelog-modal theme-${this.options.theme}`;
113
+ this.modalElement.innerHTML = `
114
+ <div class="changelog-modal-container">
115
+ <button class="changelog-modal-close" type="button" aria-label="Close">&times;</button>
116
+ <div class="changelog-modal-content">
117
+ <div class="changelog-loading">
118
+ <div class="changelog-loading-spinner"></div>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ `;
123
+
124
+ document.body.appendChild(this.modalElement);
125
+
126
+ // Prevent clicks inside modal from closing it
127
+ this.modalElement
128
+ .querySelector('.changelog-modal-container')
129
+ .addEventListener('click', (e) => {
130
+ e.stopPropagation();
131
+ });
132
+
133
+ // Attach close button event
134
+ this.modalElement
135
+ .querySelector('.changelog-modal-close')
136
+ .addEventListener('click', () => this.closeModal());
137
+
138
+ // Handle escape key
139
+ this._escapeHandler = (e) => {
140
+ if (e.key === 'Escape') {
141
+ this.closeModal();
142
+ }
143
+ };
144
+ document.addEventListener('keydown', this._escapeHandler);
145
+ }
146
+
147
+ // ==================== LIST MODAL ====================
148
+
149
+ async openSidebar() {
150
+ if (this.listModalElement) return;
151
+
152
+ // Close popup modal if open
153
+ if (this.modalElement) {
154
+ this.closeModal();
155
+ await new Promise((resolve) => setTimeout(resolve, 350));
156
+ }
157
+
158
+ this.state.isOpen = true;
159
+ this._createListModal();
160
+
161
+ // Load changelogs if not already loaded
162
+ if (this.changelogs.length === 0) {
163
+ await this._loadChangelogs();
164
+ }
165
+
166
+ this._renderChangelogList();
167
+
168
+ // Prevent body scroll
169
+ document.body.style.overflow = 'hidden';
170
+
171
+ requestAnimationFrame(() => {
172
+ if (this.listModalElement) {
173
+ this.listModalElement.classList.add('open');
174
+ // Trigger confetti animation
175
+ this._showConfetti();
176
+ }
177
+ if (this.listModalBackdropElement) {
178
+ this.listModalBackdropElement.classList.add('show');
179
+ }
180
+ });
181
+ }
182
+
183
+ _showConfetti() {
184
+ const colors = [
185
+ '#FF6B6B',
186
+ '#4ECDC4',
187
+ '#FFE66D',
188
+ '#95E1D3',
189
+ '#F38181',
190
+ '#AA96DA',
191
+ '#FCBAD3',
192
+ '#A8D8EA',
193
+ ];
194
+ const confettiCount = 50;
195
+ const container = document.createElement('div');
196
+ container.className = 'changelog-confetti-container';
197
+ document.body.appendChild(container);
198
+
199
+ for (let i = 0; i < confettiCount; i++) {
200
+ const confetti = document.createElement('div');
201
+ confetti.className = 'changelog-confetti';
202
+ confetti.style.left = Math.random() * 100 + '%';
203
+ confetti.style.backgroundColor =
204
+ colors[Math.floor(Math.random() * colors.length)];
205
+ confetti.style.animationDelay = Math.random() * 0.5 + 's';
206
+ confetti.style.animationDuration = Math.random() * 1 + 1.5 + 's';
207
+
208
+ // Random shapes
209
+ const shapes = ['circle', 'square', 'rectangle'];
210
+ const shape = shapes[Math.floor(Math.random() * shapes.length)];
211
+ if (shape === 'circle') {
212
+ confetti.style.borderRadius = '50%';
213
+ confetti.style.width = Math.random() * 8 + 4 + 'px';
214
+ confetti.style.height = confetti.style.width;
215
+ } else if (shape === 'rectangle') {
216
+ confetti.style.width = Math.random() * 4 + 3 + 'px';
217
+ confetti.style.height = Math.random() * 10 + 8 + 'px';
218
+ } else {
219
+ confetti.style.width = Math.random() * 8 + 4 + 'px';
220
+ confetti.style.height = confetti.style.width;
221
+ }
222
+
223
+ container.appendChild(confetti);
224
+ }
225
+
226
+ // Remove confetti after animation
227
+ setTimeout(() => {
228
+ if (container.parentNode) {
229
+ container.parentNode.removeChild(container);
230
+ }
231
+ }, 2500);
232
+ }
233
+
234
+ closeSidebar() {
235
+ // Restore body scroll
236
+ document.body.style.overflow = '';
237
+
238
+ if (this.listModalElement) {
239
+ this.listModalElement.classList.remove('open');
240
+ }
241
+ if (this.listModalBackdropElement) {
242
+ this.listModalBackdropElement.classList.remove('show');
243
+ }
244
+
245
+ setTimeout(() => {
246
+ this.state.isOpen = false;
247
+ if (this.listModalElement && this.listModalElement.parentNode) {
248
+ this.listModalElement.parentNode.removeChild(this.listModalElement);
249
+ this.listModalElement = null;
250
+ }
251
+ if (
252
+ this.listModalBackdropElement &&
253
+ this.listModalBackdropElement.parentNode
254
+ ) {
255
+ this.listModalBackdropElement.parentNode.removeChild(
256
+ this.listModalBackdropElement
257
+ );
258
+ this.listModalBackdropElement = null;
259
+ }
260
+ if (this._listModalEscapeHandler) {
261
+ document.removeEventListener('keydown', this._listModalEscapeHandler);
262
+ }
263
+ }, 300);
264
+ }
265
+
266
+ _createListModal() {
267
+ // Create backdrop
268
+ this.listModalBackdropElement = document.createElement('div');
269
+ this.listModalBackdropElement.className = 'changelog-list-modal-backdrop';
270
+ document.body.appendChild(this.listModalBackdropElement);
271
+ this.listModalBackdropElement.addEventListener('click', () =>
272
+ this.closeSidebar()
273
+ );
274
+
275
+ // Create list modal
276
+ this.listModalElement = document.createElement('div');
277
+ this.listModalElement.className = `changelog-list-modal theme-${this.options.theme}`;
278
+ this.listModalElement.innerHTML = `
279
+ <div class="changelog-list-modal-container">
280
+ <div class="changelog-list-modal-header">
281
+ <h2>${this.options.title || "What's New"} 🎉</h2>
282
+ <button class="changelog-list-modal-close" type="button" aria-label="Close">&times;</button>
283
+ </div>
284
+ <div class="changelog-list-modal-body">
285
+ <div class="changelog-loading">
286
+ <div class="changelog-loading-spinner"></div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ `;
291
+
292
+ document.body.appendChild(this.listModalElement);
293
+
294
+ // Prevent clicks inside modal from closing it
295
+ this.listModalElement
296
+ .querySelector('.changelog-list-modal-container')
297
+ .addEventListener('click', (e) => {
298
+ e.stopPropagation();
299
+ });
300
+
301
+ // Attach close button event
302
+ this.listModalElement
303
+ .querySelector('.changelog-list-modal-close')
304
+ .addEventListener('click', () => this.closeSidebar());
305
+
306
+ // Handle escape key
307
+ this._listModalEscapeHandler = (e) => {
308
+ if (e.key === 'Escape') {
309
+ this.closeSidebar();
310
+ }
311
+ };
312
+ document.addEventListener('keydown', this._listModalEscapeHandler);
313
+ }
314
+
315
+ _renderChangelogList() {
316
+ const body = this.listModalElement.querySelector(
317
+ '.changelog-list-modal-body'
318
+ );
319
+
320
+ if (this.isLoading) {
321
+ return;
322
+ }
323
+
324
+ if (this.changelogs.length === 0) {
325
+ body.innerHTML = `
326
+ <div class="changelog-empty">
327
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
328
+ <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
329
+ <polyline points="14,2 14,8 20,8"/>
330
+ </svg>
331
+ <p>No updates yet</p>
332
+ </div>
333
+ `;
334
+ return;
335
+ }
336
+
337
+ body.innerHTML = `
338
+ <div class="changelog-list">
339
+ ${this.changelogs.map((changelog, index) => this._renderChangelogListItem(changelog, index)).join('')}
340
+ </div>
341
+ `;
342
+
343
+ // Attach click events to items
344
+ body.querySelectorAll('.changelog-list-item').forEach((item, index) => {
345
+ item.addEventListener('click', () => {
346
+ const changelog = this.changelogs[index];
347
+ this._handleViewUpdate(changelog);
348
+ });
349
+ });
350
+ }
351
+
352
+ _renderChangelogListItem(changelog, index) {
353
+ const hasImage = changelog.cover_image || changelog.image;
354
+ const imageUrl = changelog.cover_image || changelog.image;
355
+ const date = changelog.published_at
356
+ ? this._formatDate(changelog.published_at)
357
+ : '';
358
+
359
+ return `
360
+ <div class="changelog-list-item" data-index="${index}">
361
+ <div class="changelog-list-item-main">
362
+ ${
363
+ hasImage
364
+ ? `
365
+ <div class="changelog-list-item-image">
366
+ <img src="${imageUrl}" alt="${changelog.title}" loading="lazy" />
367
+ </div>
368
+ `
369
+ : ''
370
+ }
371
+ <div class="changelog-list-item-content">
372
+ ${date ? `<span class="changelog-list-item-date">${date}</span>` : ''}
373
+ ${
374
+ changelog.labels && changelog.labels.length > 0
375
+ ? `
376
+ <div class="changelog-list-item-labels">
377
+ ${changelog.labels
378
+ .map(
379
+ (label) => `
380
+ <span class="changelog-label" style="background-color: ${label.color || '#E5E7EB'}; color: ${this._getContrastColor(label.color || '#E5E7EB')}">${label.name}</span>
381
+ `
382
+ )
383
+ .join('')}
384
+ </div>
385
+ `
386
+ : ''
387
+ }
388
+ <h3 class="changelog-list-item-title">${changelog.title}</h3>
389
+ ${
390
+ changelog.excerpt || changelog.description
391
+ ? `
392
+ <p class="changelog-list-item-description">${changelog.excerpt || changelog.description}</p>
393
+ `
394
+ : ''
395
+ }
396
+ </div>
397
+ </div>
398
+ <div class="changelog-list-item-arrow">
399
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
400
+ <path d="M9 18l6-6-6-6"/>
401
+ </svg>
402
+ </div>
403
+ </div>
404
+ `;
405
+ }
406
+
407
+ async _loadChangelogs() {
408
+ this.isLoading = true;
409
+
410
+ try {
411
+ const result = await this.apiService.getChangelogs();
412
+ this.changelogs = result.data || [];
413
+ this.sdk.eventBus.emit('changelog:loaded', {
414
+ changelogs: this.changelogs,
415
+ });
416
+ } catch (error) {
417
+ this.changelogs = [];
418
+ this.sdk.eventBus.emit('changelog:error', { error });
419
+ } finally {
420
+ this.isLoading = false;
421
+ }
422
+ }
423
+
424
+ _renderCurrentChangelog() {
425
+ const content = this.modalElement.querySelector('.changelog-modal-content');
426
+
427
+ if (this.isLoading) {
428
+ return; // Keep showing loading spinner
429
+ }
430
+
431
+ if (this.changelogs.length === 0) {
432
+ content.innerHTML = `
433
+ <div class="changelog-empty">
434
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
435
+ <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
436
+ <polyline points="14,2 14,8 20,8"/>
437
+ </svg>
438
+ <p>No updates yet</p>
439
+ </div>
440
+ `;
441
+ return;
442
+ }
443
+
444
+ const changelog = this.changelogs[this.currentIndex];
445
+ const hasImage = changelog.cover_image || changelog.image;
446
+ const imageUrl = changelog.cover_image || changelog.image;
447
+ const hasMultiple = this.changelogs.length > 1;
448
+
449
+ content.innerHTML = `
450
+ <div class="changelog-popup-item">
451
+ ${
452
+ hasImage
453
+ ? `
454
+ <div class="changelog-popup-image">
455
+ <img src="${imageUrl}" alt="${changelog.title}" loading="lazy" />
456
+ </div>
457
+ `
458
+ : ''
459
+ }
460
+ <div class="changelog-popup-body">
461
+ <h2 class="changelog-popup-title">${changelog.title}</h2>
462
+ ${
463
+ changelog.excerpt || changelog.description
464
+ ? `
465
+ <p class="changelog-popup-description">${changelog.excerpt || changelog.description}</p>
466
+ `
467
+ : ''
468
+ }
469
+ <button class="changelog-popup-btn" type="button">
470
+ ${this.options.viewButtonText || 'View Update'}
471
+ </button>
472
+ </div>
473
+ <div class="changelog-popup-footer">
474
+ ${
475
+ hasMultiple
476
+ ? `
477
+ <div class="changelog-popup-dots">
478
+ ${this.changelogs
479
+ .map(
480
+ (_, i) => `
481
+ <span class="changelog-dot ${i === this.currentIndex ? 'active' : ''}" data-index="${i}"></span>
482
+ `
483
+ )
484
+ .join('')}
485
+ </div>
486
+ `
487
+ : ''
488
+ }
489
+ <button class="changelog-view-all-btn" type="button">
490
+ View all updates
491
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
492
+ <path d="M5 12h14M12 5l7 7-7 7"/>
493
+ </svg>
494
+ </button>
495
+ </div>
496
+ </div>
497
+ `;
498
+
499
+ // Attach view button event
500
+ content
501
+ .querySelector('.changelog-popup-btn')
502
+ .addEventListener('click', () => {
503
+ this._handleViewUpdate(changelog);
504
+ });
505
+
506
+ // Attach view all button event
507
+ content
508
+ .querySelector('.changelog-view-all-btn')
509
+ .addEventListener('click', () => {
510
+ this.closeModal();
511
+ setTimeout(() => this.openSidebar(), 350);
512
+ });
513
+
514
+ // Attach dot navigation events
515
+ if (hasMultiple) {
516
+ content.querySelectorAll('.changelog-dot').forEach((dot) => {
517
+ dot.addEventListener('click', (e) => {
518
+ const index = parseInt(e.target.dataset.index, 10);
519
+ this.currentIndex = index;
520
+ this._renderCurrentChangelog();
521
+ });
522
+ });
523
+ }
524
+ }
525
+
526
+ _handleViewUpdate(changelog) {
527
+ this.sdk.eventBus.emit('changelog:view', { changelog });
528
+
529
+ // If there's a URL, open it
530
+ if (changelog.url || changelog.slug) {
531
+ const url =
532
+ changelog.url ||
533
+ `${this.options.changelogBaseUrl || ''}/${changelog.slug}`;
534
+ if (this.options.openInNewTab !== false) {
535
+ window.open(url, '_blank', 'noopener,noreferrer');
536
+ } else {
537
+ window.location.href = url;
538
+ }
539
+ }
540
+
541
+ // Custom callback if provided
542
+ if (typeof this.options.onViewUpdate === 'function') {
543
+ this.options.onViewUpdate(changelog);
544
+ }
545
+ }
546
+
547
+ _formatDate(dateString) {
548
+ const date = new Date(dateString);
549
+ const options = { year: 'numeric', month: 'short', day: 'numeric' };
550
+ return date.toLocaleDateString('en-US', options);
551
+ }
552
+
553
+ _getContrastColor(hexColor) {
554
+ // Remove # if present
555
+ const hex = hexColor.replace('#', '');
556
+ const r = parseInt(hex.substr(0, 2), 16);
557
+ const g = parseInt(hex.substr(2, 2), 16);
558
+ const b = parseInt(hex.substr(4, 2), 16);
559
+ // Calculate luminance
560
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
561
+ return luminance > 0.5 ? '#1F2937' : '#FFFFFF';
562
+ }
563
+
564
+ hideBadge() {
565
+ const badge = this.element?.querySelector('.changelog-badge');
566
+ if (badge) {
567
+ badge.style.display = 'none';
568
+ }
569
+ }
570
+
571
+ showBadge() {
572
+ const badge = this.element?.querySelector('.changelog-badge');
573
+ if (badge) {
574
+ badge.style.display = 'block';
575
+ }
576
+ }
577
+
578
+ nextChangelog() {
579
+ if (this.currentIndex < this.changelogs.length - 1) {
580
+ this.currentIndex++;
581
+ this._renderCurrentChangelog();
582
+ }
583
+ }
584
+
585
+ prevChangelog() {
586
+ if (this.currentIndex > 0) {
587
+ this.currentIndex--;
588
+ this._renderCurrentChangelog();
589
+ }
590
+ }
591
+
592
+ async refresh() {
593
+ this.changelogs = [];
594
+ await this._loadChangelogs();
595
+ if (this.modalElement) {
596
+ this.currentIndex = 0;
597
+ this._renderCurrentChangelog();
598
+ }
599
+ if (this.listModalElement) {
600
+ this._renderChangelogList();
601
+ }
602
+ }
603
+
604
+ mount(container) {
605
+ super.mount(container);
606
+ }
607
+
608
+ destroy() {
609
+ if (this._escapeHandler) {
610
+ document.removeEventListener('keydown', this._escapeHandler);
611
+ }
612
+ if (this._listModalEscapeHandler) {
613
+ document.removeEventListener('keydown', this._listModalEscapeHandler);
614
+ }
615
+ this.closeModal();
616
+ this.closeSidebar();
617
+ super.destroy();
618
+ }
619
+ }