@product7/feedback-sdk 1.2.8 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,3 @@
1
- /**
2
- * HelpView - Help collections browse with Intercom-style design
3
- */
4
1
  export class HelpView {
5
2
  constructor(state, options = {}) {
6
3
  this.state = state;
@@ -15,7 +12,6 @@ export class HelpView {
15
12
 
16
13
  this._updateContent();
17
14
 
18
- // Subscribe to state changes
19
15
  this._unsubscribe = this.state.subscribe((type) => {
20
16
  if (type === 'helpArticlesUpdate' || type === 'helpSearchChange') {
21
17
  this._updateCollectionsList();
@@ -34,7 +30,9 @@ export class HelpView {
34
30
  <div class="messenger-help-header">
35
31
  <h2>Help</h2>
36
32
  <button class="messenger-close-btn" aria-label="Close">
37
- <i class="ph ph-x" style="font-size: 20px;"></i>
33
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256">
34
+ <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path>
35
+ </svg>
38
36
  </button>
39
37
  </div>
40
38
 
@@ -46,7 +44,9 @@ export class HelpView {
46
44
  placeholder="Search for help"
47
45
  value="${searchQuery}"
48
46
  />
49
- <i class="ph ph-magnifying-glass messenger-help-search-icon" style="font-size: 18px;"></i>
47
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="#000000" viewBox="0 0 256 256" class="messenger-help-search-icon">
48
+ <path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path>
49
+ </svg>
50
50
  </div>
51
51
  </div>
52
52
 
@@ -69,7 +69,6 @@ export class HelpView {
69
69
  const collections = this.state.helpArticles || [];
70
70
  const searchQuery = (this.state.helpSearchQuery || '').toLowerCase();
71
71
 
72
- // Filter collections by search
73
72
  const filteredCollections = searchQuery
74
73
  ? collections.filter(
75
74
  (c) =>
@@ -78,7 +77,6 @@ export class HelpView {
78
77
  )
79
78
  : collections;
80
79
 
81
- // Update collection count
82
80
  const headerEl = this.element.querySelector(
83
81
  '.messenger-help-collections-header'
84
82
  );
@@ -95,7 +93,6 @@ export class HelpView {
95
93
  .map((collection) => this._renderCollectionItem(collection))
96
94
  .join('');
97
95
 
98
- // Attach click events
99
96
  this._attachCollectionEvents();
100
97
  }
101
98
 
@@ -108,7 +105,9 @@ export class HelpView {
108
105
  <p class="messenger-help-collection-desc">${collection.description || ''}</p>
109
106
  <span class="messenger-help-collection-count">${articleCount} articles</span>
110
107
  </div>
111
- <i class="ph ph-caret-right messenger-help-collection-arrow" style="font-size: 20px;"></i>
108
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256" class="messenger-help-collection-arrow">
109
+ <path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path>
110
+ </svg>
112
111
  </div>
113
112
  `;
114
113
  }
@@ -120,7 +119,9 @@ export class HelpView {
120
119
  return `
121
120
  <div class="messenger-help-empty">
122
121
  <div class="messenger-help-empty-icon">
123
- <i class="ph ph-magnifying-glass" style="font-size: 48px;"></i>
122
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="#000000" viewBox="0 0 256 256">
123
+ <path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z"></path>
124
+ </svg>
124
125
  </div>
125
126
  <h3>No results found</h3>
126
127
  <p>Try a different search term</p>
@@ -131,7 +132,9 @@ export class HelpView {
131
132
  return `
132
133
  <div class="messenger-help-empty">
133
134
  <div class="messenger-help-empty-icon">
134
- <i class="ph ph-question" style="font-size: 48px;"></i>
135
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="#000000" viewBox="0 0 256 256">
136
+ <path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path>
137
+ </svg>
135
138
  </div>
136
139
  <h3>Help collections</h3>
137
140
  <p>No collections available yet</p>
@@ -140,14 +143,12 @@ export class HelpView {
140
143
  }
141
144
 
142
145
  _attachEvents() {
143
- // Close button
144
146
  this.element
145
147
  .querySelector('.messenger-close-btn')
146
148
  .addEventListener('click', () => {
147
149
  this.state.setOpen(false);
148
150
  });
149
151
 
150
- // Search input
151
152
  const searchInput = this.element.querySelector(
152
153
  '.messenger-help-search-input'
153
154
  );
@@ -1,6 +1,3 @@
1
- /**
2
- * HomeView - Welcome screen with team info and quick actions
3
- */
4
1
  export class HomeView {
5
2
  constructor(state, options = {}) {
6
3
  this.state = state;
@@ -15,7 +12,6 @@ export class HomeView {
15
12
 
16
13
  this._updateContent();
17
14
 
18
- // Subscribe to state changes to re-render when data loads
19
15
  this._unsubscribe = this.state.subscribe((type) => {
20
16
  if (
21
17
  type === 'homeChangelogUpdate' ||
@@ -41,7 +37,9 @@ export class HomeView {
41
37
  </div>
42
38
  <div class="messenger-home-avatars">${avatarsHtml}</div>
43
39
  <button class="messenger-close-btn" aria-label="Close">
44
- <i class="ph ph-x" style="font-size: 20px;"></i>
40
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256">
41
+ <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path>
42
+ </svg>
45
43
  </button>
46
44
  </div>
47
45
  <div class="messenger-home-welcome">
@@ -52,10 +50,7 @@ export class HomeView {
52
50
  </div>
53
51
 
54
52
  <div class="messenger-home-body">
55
- <button class="messenger-home-message-btn">
56
- <span>Send us a message</span>
57
- <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
58
- </button>
53
+ ${this._renderMessageButton()}
59
54
 
60
55
  ${this._renderFeaturedCard()}
61
56
 
@@ -69,7 +64,6 @@ export class HomeView {
69
64
  _renderAvatarStack() {
70
65
  const avatars = this.state.teamAvatars;
71
66
  if (!avatars || avatars.length === 0) {
72
- // Default avatars with initials
73
67
  return `
74
68
  <div class="messenger-avatar-stack">
75
69
  <div class="messenger-avatar" style="background: #5856d6;">S</div>
@@ -118,8 +112,38 @@ export class HomeView {
118
112
  `;
119
113
  }
120
114
 
115
+ _renderMessageButton() {
116
+ const openConversation = this.state.conversations.find(
117
+ (c) => c.status === 'open'
118
+ );
119
+
120
+ if (openConversation) {
121
+ const preview = openConversation.lastMessage
122
+ ? (openConversation.lastMessage.length > 40
123
+ ? openConversation.lastMessage.substring(0, 40) + '...'
124
+ : openConversation.lastMessage)
125
+ : 'Continue your conversation';
126
+
127
+ return `
128
+ <button class="messenger-home-message-btn messenger-home-continue-btn" data-conversation-id="${openConversation.id}">
129
+ <div class="messenger-home-continue-info">
130
+ <span class="messenger-home-continue-label">Continue conversation</span>
131
+ <span class="messenger-home-continue-preview">${preview}</span>
132
+ </div>
133
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
134
+ </button>
135
+ `;
136
+ }
137
+
138
+ return `
139
+ <button class="messenger-home-message-btn">
140
+ <span>Send us a message</span>
141
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
142
+ </button>
143
+ `;
144
+ }
145
+
121
146
  _renderFeaturedCard() {
122
- // Only show if there's featured content configured
123
147
  if (!this.options.featuredContent) {
124
148
  return '';
125
149
  }
@@ -140,7 +164,6 @@ export class HomeView {
140
164
  }
141
165
 
142
166
  _renderRecentChangelog() {
143
- // Show recent changelog preview as cards with images
144
167
  const changelogItems = this.state.homeChangelogItems;
145
168
  if (changelogItems.length === 0) {
146
169
  return '';
@@ -189,31 +212,40 @@ export class HomeView {
189
212
  }
190
213
 
191
214
  _attachEvents() {
192
- // Close button
193
215
  this.element
194
216
  .querySelector('.messenger-close-btn')
195
217
  .addEventListener('click', () => {
196
218
  this.state.setOpen(false);
197
219
  });
198
220
 
199
- // Send message button
200
- this.element
201
- .querySelector('.messenger-home-message-btn')
202
- .addEventListener('click', () => {
203
- this.state.setView('messages');
221
+ // Send message / continue conversation button
222
+ const msgBtn = this.element.querySelector('.messenger-home-message-btn');
223
+ if (msgBtn) {
224
+ msgBtn.addEventListener('click', () => {
225
+ const convId = msgBtn.dataset.conversationId;
226
+ if (convId) {
227
+ // Continue existing open conversation
228
+ this.state.setActiveConversation(convId);
229
+ this.state.setView('chat');
230
+ if (this.options.onSelectConversation) {
231
+ this.options.onSelectConversation(convId);
232
+ }
233
+ } else {
234
+ // No open conversation — start new
235
+ this.state.setActiveConversation(null);
236
+ this.state.setView('chat');
237
+ }
204
238
  });
239
+ }
205
240
 
206
- // Changelog items
207
241
  this.element
208
242
  .querySelectorAll('.messenger-home-changelog-item')
209
243
  .forEach((item) => {
210
244
  item.addEventListener('click', () => {
211
- // Navigate to changelog view with specific item selected
212
245
  this.state.setView('changelog');
213
246
  });
214
247
  });
215
248
 
216
- // See all changelog
217
249
  const seeAllBtn = this.element.querySelector(
218
250
  '.messenger-home-changelog-all'
219
251
  );
@@ -223,7 +255,6 @@ export class HomeView {
223
255
  });
224
256
  }
225
257
 
226
- // Featured card action
227
258
  const featuredBtn = this.element.querySelector(
228
259
  '.messenger-home-featured-btn'
229
260
  );
@@ -0,0 +1,224 @@
1
+ /**
2
+ * PreChatFormView - Collects user info after the first message
3
+ */
4
+ export class PreChatFormView {
5
+ constructor(state, options = {}) {
6
+ this.state = state;
7
+ this.options = options;
8
+ this.element = null;
9
+ this._isSubmitting = false;
10
+ }
11
+
12
+ render() {
13
+ this.element = document.createElement('div');
14
+ this.element.className = 'messenger-view messenger-prechat-view';
15
+
16
+ this._updateContent();
17
+
18
+ return this.element;
19
+ }
20
+
21
+ _updateContent() {
22
+ // Pre-fill from userContext if available
23
+ const existingName = this.state.userContext?.name || '';
24
+ const existingEmail = this.state.userContext?.email || '';
25
+
26
+ this.element.innerHTML = `
27
+ <div class="messenger-prechat-overlay">
28
+ <div class="messenger-prechat-card">
29
+ <h4>Get notified when we reply</h4>
30
+ <form class="messenger-prechat-form" novalidate>
31
+ <div class="messenger-prechat-fields">
32
+ <input
33
+ type="text"
34
+ id="messenger-prechat-name"
35
+ name="name"
36
+ placeholder="Name (optional)"
37
+ value="${this._escapeHtml(existingName)}"
38
+ autocomplete="name"
39
+ class="messenger-prechat-input"
40
+ />
41
+ <input
42
+ type="email"
43
+ id="messenger-prechat-email"
44
+ name="email"
45
+ placeholder="Email address"
46
+ value="${this._escapeHtml(existingEmail)}"
47
+ required
48
+ autocomplete="email"
49
+ class="messenger-prechat-input"
50
+ />
51
+ </div>
52
+ <span class="messenger-prechat-error" id="messenger-email-error"></span>
53
+ <div class="messenger-prechat-actions">
54
+ <button type="button" class="messenger-prechat-skip">Skip</button>
55
+ <button type="submit" class="messenger-prechat-submit" disabled>
56
+ <span class="messenger-prechat-submit-text">Continue</span>
57
+ <span class="messenger-prechat-submit-loading" style="display: none;">
58
+ <i class="ph ph-spinner" style="font-size: 16px;"></i>
59
+ </span>
60
+ </button>
61
+ </div>
62
+ </form>
63
+ </div>
64
+ </div>
65
+ `;
66
+
67
+ this._attachEvents();
68
+ }
69
+
70
+ _renderTeamAvatars() {
71
+ const avatars = this.state.teamAvatars;
72
+ if (!avatars || avatars.length === 0) {
73
+ return `
74
+ <div class="messenger-avatar-stack">
75
+ <div class="messenger-avatar" style="background: #5856d6;">S</div>
76
+ <div class="messenger-avatar" style="background: #007aff;">T</div>
77
+ </div>
78
+ `;
79
+ }
80
+
81
+ const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
82
+ const avatarItems = avatars
83
+ .slice(0, 3)
84
+ .map((avatar, i) => {
85
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
86
+ return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
87
+ }
88
+ return `<div class="messenger-avatar" style="background: ${colors[i % colors.length]}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
89
+ })
90
+ .join('');
91
+
92
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
93
+ }
94
+
95
+ _escapeHtml(text) {
96
+ if (!text) return '';
97
+ return text
98
+ .replace(/&/g, '&amp;')
99
+ .replace(/</g, '&lt;')
100
+ .replace(/>/g, '&gt;')
101
+ .replace(/"/g, '&quot;');
102
+ }
103
+
104
+ _attachEvents() {
105
+ // Form validation
106
+ const form = this.element.querySelector('.messenger-prechat-form');
107
+ const emailInput = this.element.querySelector('#messenger-prechat-email');
108
+ const submitBtn = this.element.querySelector('.messenger-prechat-submit');
109
+ const skipBtn = this.element.querySelector('.messenger-prechat-skip');
110
+
111
+ const validateForm = () => {
112
+ const email = emailInput.value.trim();
113
+ const isEmailValid = this._isValidEmail(email);
114
+ submitBtn.disabled = !isEmailValid;
115
+ return isEmailValid;
116
+ };
117
+
118
+ emailInput.addEventListener('input', () => {
119
+ this._clearError('messenger-email-error');
120
+ validateForm();
121
+ });
122
+
123
+ emailInput.addEventListener('blur', () => {
124
+ const email = emailInput.value.trim();
125
+ if (email && !this._isValidEmail(email)) {
126
+ this._showError('messenger-email-error', 'Please enter a valid email');
127
+ }
128
+ });
129
+
130
+ // Skip button - go back to chat without collecting info
131
+ skipBtn.addEventListener('click', () => {
132
+ this.state.setView('chat');
133
+ });
134
+
135
+ // Form submission
136
+ form.addEventListener('submit', async (e) => {
137
+ e.preventDefault();
138
+ if (this._isSubmitting) return;
139
+ if (!validateForm()) {
140
+ this._showError('messenger-email-error', 'Please enter a valid email');
141
+ return;
142
+ }
143
+ await this._handleSubmit();
144
+ });
145
+
146
+ // Set initial button state
147
+ validateForm();
148
+
149
+ // Focus email input
150
+ setTimeout(() => emailInput.focus(), 100);
151
+ }
152
+
153
+ _isValidEmail(email) {
154
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
155
+ return emailRegex.test(email);
156
+ }
157
+
158
+ _showError(elementId, message) {
159
+ const errorEl = this.element.querySelector(`#${elementId}`);
160
+ if (errorEl) {
161
+ errorEl.textContent = message;
162
+ errorEl.style.display = 'block';
163
+ }
164
+ }
165
+
166
+ _clearError(elementId) {
167
+ const errorEl = this.element.querySelector(`#${elementId}`);
168
+ if (errorEl) {
169
+ errorEl.textContent = '';
170
+ errorEl.style.display = 'none';
171
+ }
172
+ }
173
+
174
+ async _handleSubmit() {
175
+ const nameInput = this.element.querySelector('#messenger-prechat-name');
176
+ const emailInput = this.element.querySelector('#messenger-prechat-email');
177
+ const submitBtn = this.element.querySelector('.messenger-prechat-submit');
178
+ const submitText = submitBtn.querySelector('.messenger-prechat-submit-text');
179
+ const submitLoading = submitBtn.querySelector('.messenger-prechat-submit-loading');
180
+
181
+ const name = nameInput.value.trim();
182
+ const email = emailInput.value.trim();
183
+
184
+ // Show loading state
185
+ this._isSubmitting = true;
186
+ submitBtn.disabled = true;
187
+ submitText.style.display = 'none';
188
+ submitLoading.style.display = 'inline-flex';
189
+
190
+ try {
191
+ // First, identify the contact with collected info
192
+ if (this.options.onIdentifyContact) {
193
+ await this.options.onIdentifyContact({ name, email });
194
+ }
195
+
196
+ // Update state with user info
197
+ if (!this.state.userContext) {
198
+ this.state.userContext = {};
199
+ }
200
+ this.state.userContext.name = name;
201
+ this.state.userContext.email = email;
202
+
203
+ this._isSubmitting = false;
204
+
205
+ // Go to chat after collecting contact info
206
+ this.state.setView('chat');
207
+ } catch (error) {
208
+ console.error('[PreChatFormView] Error submitting form:', error);
209
+ this._showError('messenger-email-error', 'Something went wrong. Please try again.');
210
+
211
+ // Reset button state
212
+ this._isSubmitting = false;
213
+ submitBtn.disabled = false;
214
+ submitText.style.display = 'inline';
215
+ submitLoading.style.display = 'none';
216
+ }
217
+ }
218
+
219
+ destroy() {
220
+ if (this.element && this.element.parentNode) {
221
+ this.element.parentNode.removeChild(this.element);
222
+ }
223
+ }
224
+ }