@product7/product7-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1025 -0
  2. package/dist/README.md +1025 -0
  3. package/dist/product7-js.js +14658 -0
  4. package/dist/product7-js.js.map +1 -0
  5. package/dist/product7-js.min.js +2 -0
  6. package/dist/product7-js.min.js.map +1 -0
  7. package/package.json +114 -0
  8. package/src/api/mock-data/index.js +360 -0
  9. package/src/api/services/ChangelogService.js +28 -0
  10. package/src/api/services/FeedbackService.js +44 -0
  11. package/src/api/services/HelpService.js +50 -0
  12. package/src/api/services/MessengerService.js +279 -0
  13. package/src/api/services/SurveyService.js +127 -0
  14. package/src/api/utils/helpers.js +30 -0
  15. package/src/core/APIService.js +303 -0
  16. package/src/core/BaseAPIService.js +298 -0
  17. package/src/core/EventBus.js +54 -0
  18. package/src/core/Product7.js +812 -0
  19. package/src/core/WebSocketService.js +275 -0
  20. package/src/docs/api.md +226 -0
  21. package/src/docs/example.md +461 -0
  22. package/src/docs/framework-integrations.md +714 -0
  23. package/src/docs/installation.md +281 -0
  24. package/src/index.js +894 -0
  25. package/src/styles/base.js +50 -0
  26. package/src/styles/changelog.js +665 -0
  27. package/src/styles/components.js +553 -0
  28. package/src/styles/design-tokens.js +124 -0
  29. package/src/styles/feedback.js +325 -0
  30. package/src/styles/messenger-components.js +632 -0
  31. package/src/styles/messenger-core.js +233 -0
  32. package/src/styles/messenger-features.js +169 -0
  33. package/src/styles/messenger-views.js +877 -0
  34. package/src/styles/messenger.js +17 -0
  35. package/src/styles/messengerCustomStyles.js +114 -0
  36. package/src/styles/styles.js +26 -0
  37. package/src/styles/survey.js +894 -0
  38. package/src/utils/errors.js +142 -0
  39. package/src/utils/helpers.js +219 -0
  40. package/src/widgets/BaseWidget.js +548 -0
  41. package/src/widgets/ButtonWidget.js +104 -0
  42. package/src/widgets/ChangelogWidget.js +615 -0
  43. package/src/widgets/InlineWidget.js +148 -0
  44. package/src/widgets/MessengerWidget.js +979 -0
  45. package/src/widgets/SurveyWidget.js +1325 -0
  46. package/src/widgets/TabWidget.js +45 -0
  47. package/src/widgets/WidgetFactory.js +70 -0
  48. package/src/widgets/messenger/MessengerState.js +323 -0
  49. package/src/widgets/messenger/components/MessengerLauncher.js +124 -0
  50. package/src/widgets/messenger/components/MessengerPanel.js +111 -0
  51. package/src/widgets/messenger/components/NavigationTabs.js +130 -0
  52. package/src/widgets/messenger/views/ChangelogView.js +167 -0
  53. package/src/widgets/messenger/views/ChatView.js +592 -0
  54. package/src/widgets/messenger/views/ConversationsView.js +244 -0
  55. package/src/widgets/messenger/views/HelpView.js +239 -0
  56. package/src/widgets/messenger/views/HomeView.js +300 -0
  57. package/src/widgets/messenger/views/PreChatFormView.js +109 -0
  58. package/types/index.d.ts +341 -0
@@ -0,0 +1,244 @@
1
+ export class ConversationsView {
2
+ constructor(state, options = {}) {
3
+ this.state = state;
4
+ this.options = options;
5
+ this.element = null;
6
+ this._unsubscribe = null;
7
+ this.avatarColors = [
8
+ '#155EEF',
9
+ '#8b5cf6',
10
+ '#10b981',
11
+ '#f59e0b',
12
+ '#ef4444',
13
+ '#ec4899',
14
+ '#06b6d4',
15
+ ];
16
+ }
17
+
18
+ render() {
19
+ this.element = document.createElement('div');
20
+ this.element.className = 'messenger-view messenger-conversations-view';
21
+
22
+ this._updateContent();
23
+ this._attachEvents();
24
+
25
+ this._unsubscribe = this.state.subscribe((type) => {
26
+ if (
27
+ type === 'conversationsUpdate' ||
28
+ type === 'conversationAdded' ||
29
+ type === 'conversationRead' ||
30
+ type === 'conversationUpdated'
31
+ ) {
32
+ this._updateContent();
33
+ }
34
+ });
35
+
36
+ return this.element;
37
+ }
38
+
39
+ _getAvatarColor(name) {
40
+ // Generate consistent color based on name
41
+ const charCode = (name || 'S').charCodeAt(0);
42
+ return this.avatarColors[charCode % this.avatarColors.length];
43
+ }
44
+
45
+ _updateContent() {
46
+ const conversations = this.state.conversations;
47
+
48
+ let conversationsHtml;
49
+ if (conversations.length === 0) {
50
+ conversationsHtml = `
51
+ <div class="messenger-empty-state">
52
+ <div class="messenger-empty-state-icon">
53
+ <iconify-icon icon="ph:chat-circle-duotone" width="48" height="48"></iconify-icon>
54
+ </div>
55
+ <h3>No conversations yet</h3>
56
+ <p>Start a new conversation with our team</p>
57
+ </div>
58
+ `;
59
+ } else {
60
+ conversationsHtml = `
61
+ <div class="messenger-conversations-list">
62
+ ${conversations.map((conv) => this._renderConversationItem(conv)).join('')}
63
+ </div>
64
+ `;
65
+ }
66
+
67
+ this.element.innerHTML = `
68
+ <div class="messenger-conversations-header">
69
+ <h2>Messages</h2>
70
+ <button class="sdk-close-btn" aria-label="Close">
71
+ <iconify-icon icon="ph:x-duotone" width="18" height="18"></iconify-icon>
72
+ </button>
73
+ </div>
74
+
75
+ <div class="messenger-conversations-body">
76
+ ${conversationsHtml}
77
+ </div>
78
+
79
+ <div class="messenger-conversations-footer">
80
+ <button class="messenger-new-message-btn">
81
+ <span>Send us a message</span>
82
+ <iconify-icon icon="ph:paper-plane-right" width="20" height="20" style="flex-shrink: 0;"></iconify-icon>
83
+ </button>
84
+ </div>
85
+ `;
86
+
87
+ this._attachEvents();
88
+ }
89
+
90
+ _renderConversationItem(conversation) {
91
+ const unreadClass = conversation.unread > 0 ? 'unread' : '';
92
+ const timeAgo = this._formatTimeAgo(conversation.lastMessageTime);
93
+ const avatarsHtml = this._renderConversationAvatars(
94
+ conversation.participants
95
+ );
96
+
97
+ return `
98
+ <div class="messenger-conversation-item ${unreadClass}" data-conversation-id="${conversation.id}">
99
+ <div class="messenger-conversation-avatars">
100
+ ${avatarsHtml}
101
+ </div>
102
+ <div class="messenger-conversation-content">
103
+ <div class="messenger-conversation-header">
104
+ <span class="messenger-conversation-title">${conversation.title || 'Chat with team'}</span>
105
+ <span class="messenger-conversation-time">${timeAgo}</span>
106
+ </div>
107
+ <div class="messenger-conversation-preview">
108
+ ${conversation.unread > 0 ? '<span class="messenger-unread-dot"></span>' : ''}
109
+ <span class="messenger-conversation-message">${this._truncateMessage(conversation.lastMessage)}</span>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ `;
114
+ }
115
+
116
+ _renderConversationAvatars(participants) {
117
+ if (!participants || participants.length === 0) {
118
+ const color = this._getAvatarColor('S');
119
+ return `<div class="sdk-avatar sdk-avatar-md" style="background-color: ${color};">S</div>`;
120
+ }
121
+
122
+ const p = participants[0];
123
+ if (p.avatarUrl) {
124
+ return `<div class="sdk-avatar sdk-avatar-md"><img src="${p.avatarUrl}" alt="${p.name}" /></div>`;
125
+ }
126
+ const initial = (p.name || 'S').charAt(0).toUpperCase();
127
+ const color = this._getAvatarColor(p.name);
128
+ return `<div class="sdk-avatar sdk-avatar-md" style="background-color: ${color};">${initial}</div>`;
129
+ }
130
+
131
+ _renderAvatarStack() {
132
+ const avatars = this.state.teamAvatars;
133
+ if (!avatars || avatars.length === 0) {
134
+ const color1 = this._getAvatarColor('S');
135
+ const color2 = this._getAvatarColor('T');
136
+ return `
137
+ <div class="messenger-avatar-stack">
138
+ <div class="sdk-avatar sdk-avatar-sm" style="background-color: ${color1};">S</div>
139
+ <div class="sdk-avatar sdk-avatar-sm" style="background-color: ${color2};">T</div>
140
+ </div>
141
+ `;
142
+ }
143
+
144
+ const avatarItems = avatars
145
+ .slice(0, 2)
146
+ .map((avatar, index) => {
147
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
148
+ return `<div class="sdk-avatar sdk-avatar-sm"><img src="${avatar}" alt="Team member" /></div>`;
149
+ }
150
+ const initial = avatar.charAt(0).toUpperCase();
151
+ const color = this._getAvatarColor(avatar);
152
+ return `<div class="sdk-avatar sdk-avatar-sm" style="background-color: ${color};">${initial}</div>`;
153
+ })
154
+ .join('');
155
+
156
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
157
+ }
158
+
159
+ _formatTimeAgo(timestamp) {
160
+ if (!timestamp) return '';
161
+ const date = new Date(timestamp);
162
+ const now = new Date();
163
+ const diffMs = now - date;
164
+ const diffMins = Math.floor(diffMs / 60000);
165
+ const diffHours = Math.floor(diffMs / 3600000);
166
+ const diffDays = Math.floor(diffMs / 86400000);
167
+
168
+ if (diffMins < 1) return 'now';
169
+ if (diffMins < 60) return `${diffMins}m`;
170
+ if (diffHours < 24) return `${diffHours}h`;
171
+ if (diffDays < 7) return `${diffDays}d`;
172
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
173
+ }
174
+
175
+ _truncateMessage(message) {
176
+ if (!message) return 'No messages yet';
177
+ const maxLength = 50;
178
+ if (message.length <= maxLength) return message;
179
+ return message.substring(0, maxLength) + '...';
180
+ }
181
+
182
+ _attachEvents() {
183
+ const closeBtn = this.element.querySelector('.sdk-close-btn');
184
+ if (closeBtn) {
185
+ closeBtn.addEventListener('click', () => {
186
+ this.state.setOpen(false);
187
+ });
188
+ }
189
+
190
+ this.element
191
+ .querySelectorAll('.messenger-conversation-item')
192
+ .forEach((item) => {
193
+ item.addEventListener('click', () => {
194
+ const convId = item.dataset.conversationId;
195
+ this.state.setActiveConversation(convId);
196
+ this.state.markAsRead(convId);
197
+ this.state.setView('chat');
198
+
199
+ if (this.options.onSelectConversation) {
200
+ this.options.onSelectConversation(convId);
201
+ }
202
+ });
203
+ });
204
+
205
+ const newMsgBtn = this.element.querySelector('.messenger-new-message-btn');
206
+ if (newMsgBtn) {
207
+ newMsgBtn.addEventListener('click', () => {
208
+ this._startNewConversation();
209
+ });
210
+ }
211
+ }
212
+
213
+ _startNewConversation() {
214
+ // If there's an open conversation, route to it instead of creating new
215
+ const openConversation = this.state.conversations.find(
216
+ (c) => c.status === 'open'
217
+ );
218
+
219
+ if (openConversation) {
220
+ this.state.setActiveConversation(openConversation.id);
221
+ this.state.markAsRead(openConversation.id);
222
+ this.state.setView('chat');
223
+ if (this.options.onSelectConversation) {
224
+ this.options.onSelectConversation(openConversation.id);
225
+ }
226
+ } else {
227
+ this.state.setActiveConversation(null);
228
+ if (this.options.onStartNewConversation) {
229
+ this.options.onStartNewConversation();
230
+ } else {
231
+ this.state.setView('chat');
232
+ }
233
+ }
234
+ }
235
+
236
+ destroy() {
237
+ if (this._unsubscribe) {
238
+ this._unsubscribe();
239
+ }
240
+ if (this.element && this.element.parentNode) {
241
+ this.element.parentNode.removeChild(this.element);
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,239 @@
1
+ export class HelpView {
2
+ constructor(state, options = {}) {
3
+ this.state = state;
4
+ this.options = options;
5
+ this.element = null;
6
+ this._unsubscribe = null;
7
+ }
8
+
9
+ render() {
10
+ this.element = document.createElement('div');
11
+ this.element.className = 'messenger-view messenger-help-view';
12
+
13
+ this._updateContent();
14
+
15
+ this._unsubscribe = this.state.subscribe((type) => {
16
+ if (type === 'helpArticlesUpdate' || type === 'helpSearchChange') {
17
+ this._updateCollectionsList();
18
+ }
19
+ });
20
+
21
+ return this.element;
22
+ }
23
+
24
+ _updateContent() {
25
+ const searchQuery = this.state.helpSearchQuery || '';
26
+
27
+ this.element.innerHTML = `
28
+ <div class="messenger-help-header">
29
+ <div class="messenger-help-header-top">
30
+ <h2>Help</h2>
31
+ <button class="sdk-close-btn messenger-help-close-btn" aria-label="Close">
32
+ <iconify-icon icon="ph:x-duotone" width="18" height="18"></iconify-icon>
33
+ </button>
34
+ </div>
35
+ <div class="messenger-help-search-wrap">
36
+ <span class="messenger-help-search-icon">
37
+ <iconify-icon icon="ph:magnifying-glass-duotone" width="16" height="16"></iconify-icon>
38
+ </span>
39
+ <input
40
+ type="text"
41
+ class="messenger-help-search-input"
42
+ placeholder="Search for help..."
43
+ value="${searchQuery}"
44
+ />
45
+ </div>
46
+ </div>
47
+ <div class="messenger-help-body">
48
+ <div class="messenger-help-collections"></div>
49
+ </div>
50
+ `;
51
+
52
+ this._updateCollectionsList();
53
+ this._attachEvents();
54
+ }
55
+
56
+ _updateCollectionsList() {
57
+ const container = this.element.querySelector('.messenger-help-collections');
58
+ const collections = this.state.helpArticles || [];
59
+ const searchQuery = (this.state.helpSearchQuery || '').toLowerCase();
60
+
61
+ const filtered = searchQuery
62
+ ? collections.filter(
63
+ (c) =>
64
+ c.title.toLowerCase().includes(searchQuery) ||
65
+ (c.description && c.description.toLowerCase().includes(searchQuery))
66
+ )
67
+ : collections;
68
+
69
+ if (filtered.length === 0) {
70
+ container.innerHTML = this._renderEmptyState();
71
+ return;
72
+ }
73
+
74
+ container.innerHTML = filtered
75
+ .map((c) => this._renderCollectionItem(c))
76
+ .join('');
77
+ this._attachCollectionEvents();
78
+ }
79
+
80
+ _avatarColors = [
81
+ { bg: '#EF4444', text: '#FFFFFF' },
82
+ { bg: '#F97316', text: '#FFFFFF' },
83
+ { bg: '#F59E0B', text: '#FFFFFF' },
84
+ { bg: '#10B981', text: '#FFFFFF' },
85
+ { bg: '#06B6D4', text: '#FFFFFF' },
86
+ { bg: '#3B82F6', text: '#FFFFFF' },
87
+ { bg: '#8B5CF6', text: '#FFFFFF' },
88
+ { bg: '#EC4899', text: '#FFFFFF' },
89
+ ];
90
+
91
+ _getAvatarColor(id) {
92
+ const hash = id
93
+ .split('')
94
+ .reduce((acc, char) => acc + char.charCodeAt(0), 0);
95
+ return this._avatarColors[hash % this._avatarColors.length];
96
+ }
97
+
98
+ _getInitials(name) {
99
+ if (!name) return 'A';
100
+ return name
101
+ .split(' ')
102
+ .map((n) => n[0])
103
+ .join('')
104
+ .toUpperCase()
105
+ .slice(0, 2);
106
+ }
107
+
108
+ _renderAuthorAvatar(collection) {
109
+ if (collection.author?.picture) {
110
+ return `<img
111
+ src="${collection.author.picture}"
112
+ alt="${collection.author.name || ''}"
113
+ class="messenger-help-collection-avatar"
114
+ title="${collection.author.name || ''}"
115
+ />`;
116
+ }
117
+
118
+ const { bg, text } = this._getAvatarColor(collection.id);
119
+ const initials = collection.author?.name
120
+ ? this._getInitials(collection.author.name)
121
+ : 'A';
122
+
123
+ return `<span
124
+ class="messenger-help-collection-avatar messenger-help-collection-avatar--initials"
125
+ style="background-color: ${bg}; color: ${text};"
126
+ title="${collection.author?.name || 'Author'}"
127
+ >${initials}</span>`;
128
+ }
129
+
130
+ _resolveCollectionIcon(icon) {
131
+ if (!icon) return this._defaultCollectionIcon();
132
+
133
+ if (icon.trimStart().startsWith('<')) {
134
+ return `<span class="messenger-help-collection-icon">${icon}</span>`;
135
+ }
136
+
137
+ if (icon.startsWith('ph:')) {
138
+ return `<span class="messenger-help-collection-icon">
139
+ <iconify-icon icon="${icon}" width="22" height="22"></iconify-icon>
140
+ </span>`;
141
+ }
142
+
143
+ return this._defaultCollectionIcon();
144
+ }
145
+
146
+ _defaultCollectionIcon() {
147
+ return `<span class="messenger-help-collection-icon">
148
+ <iconify-icon icon="ph:book-open-duotone" width="22" height="22"></iconify-icon>
149
+ </span>`;
150
+ }
151
+
152
+ _renderCollectionItem(collection) {
153
+ const articleCount = collection.articleCount || 0;
154
+
155
+ return `
156
+ <div class="messenger-help-collection" data-collection-id="${collection.id}">
157
+ ${this._resolveCollectionIcon(collection.icon)}
158
+ <div class="messenger-help-collection-content">
159
+ <div class="messenger-help-collection-title">${collection.title}</div>
160
+ ${collection.description ? `<p class="messenger-help-collection-desc">${collection.description}</p>` : ''}
161
+ <div class="messenger-help-collection-meta">
162
+ ${this._renderAuthorAvatar(collection)}
163
+ <span class="messenger-help-collection-count">
164
+ ${articleCount} ${articleCount === 1 ? 'article' : 'articles'}
165
+ </span>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ `;
170
+ }
171
+
172
+ _renderEmptyState() {
173
+ if (this.state.helpSearchQuery) {
174
+ return `
175
+ <div class="messenger-empty-state">
176
+ <div class="messenger-empty-state-icon">
177
+ <iconify-icon icon="ph:magnifying-glass-duotone" width="48" height="48"></iconify-icon>
178
+ </div>
179
+ <h3>No results found</h3>
180
+ <p>Try a different search term</p>
181
+ </div>
182
+ `;
183
+ }
184
+
185
+ return `
186
+ <div class="messenger-empty-state">
187
+ <div class="messenger-empty-state-icon">
188
+ <iconify-icon icon="ph:books-duotone" width="48" height="48"></iconify-icon>
189
+ </div>
190
+ <h3>Help collections</h3>
191
+ <p>No collections available yet</p>
192
+ </div>
193
+ `;
194
+ }
195
+
196
+ _attachEvents() {
197
+ this.element
198
+ .querySelector('.messenger-help-close-btn')
199
+ .addEventListener('click', () => {
200
+ this.state.setOpen(false);
201
+ });
202
+
203
+ const searchInput = this.element.querySelector(
204
+ '.messenger-help-search-input'
205
+ );
206
+ let searchTimeout;
207
+ searchInput.addEventListener('input', (e) => {
208
+ clearTimeout(searchTimeout);
209
+ searchTimeout = setTimeout(() => {
210
+ this.state.setHelpSearchQuery(e.target.value);
211
+ }, 300);
212
+ });
213
+
214
+ this._attachCollectionEvents();
215
+ }
216
+
217
+ _attachCollectionEvents() {
218
+ this.element
219
+ .querySelectorAll('.messenger-help-collection')
220
+ .forEach((item) => {
221
+ item.addEventListener('click', () => {
222
+ const collection = this.state.helpArticles.find(
223
+ (c) => c.id === item.dataset.collectionId
224
+ );
225
+ if (collection?.url) {
226
+ window.open(collection.url, '_blank');
227
+ } else if (this.options.onArticleClick) {
228
+ this.options.onArticleClick(collection);
229
+ }
230
+ });
231
+ });
232
+ }
233
+
234
+ destroy() {
235
+ if (this._unsubscribe) this._unsubscribe();
236
+ if (this.element?.parentNode)
237
+ this.element.parentNode.removeChild(this.element);
238
+ }
239
+ }