@product7/feedback-sdk 1.1.8 → 1.2.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.
@@ -0,0 +1,191 @@
1
+ /**
2
+ * HelpView - Help collections browse with Intercom-style design
3
+ */
4
+ export class HelpView {
5
+ constructor(state, options = {}) {
6
+ this.state = state;
7
+ this.options = options;
8
+ this.element = null;
9
+ this._unsubscribe = null;
10
+ }
11
+
12
+ render() {
13
+ this.element = document.createElement('div');
14
+ this.element.className = 'messenger-view messenger-help-view';
15
+
16
+ this._updateContent();
17
+
18
+ // Subscribe to state changes
19
+ this._unsubscribe = this.state.subscribe((type) => {
20
+ if (type === 'helpArticlesUpdate' || type === 'helpSearchChange') {
21
+ this._updateCollectionsList();
22
+ }
23
+ });
24
+
25
+ return this.element;
26
+ }
27
+
28
+ _updateContent() {
29
+ const searchQuery = this.state.helpSearchQuery || '';
30
+ const collections = this.state.helpArticles || [];
31
+ const collectionCount = collections.length;
32
+
33
+ this.element.innerHTML = `
34
+ <div class="messenger-help-header">
35
+ <h2>Help</h2>
36
+ <button class="messenger-close-btn" aria-label="Close">
37
+ <i class="ph ph-x" style="font-size: 20px;"></i>
38
+ </button>
39
+ </div>
40
+
41
+ <div class="messenger-help-search">
42
+ <div class="messenger-help-search-wrapper">
43
+ <input
44
+ type="text"
45
+ class="messenger-help-search-input"
46
+ placeholder="Search for help"
47
+ value="${searchQuery}"
48
+ />
49
+ <i class="ph ph-magnifying-glass messenger-help-search-icon" style="font-size: 18px;"></i>
50
+ </div>
51
+ </div>
52
+
53
+ <div class="messenger-help-body">
54
+ <div class="messenger-help-collections-header">
55
+ ${collectionCount} collections
56
+ </div>
57
+ <div class="messenger-help-collections"></div>
58
+ </div>
59
+ `;
60
+
61
+ this._updateCollectionsList();
62
+ this._attachEvents();
63
+ }
64
+
65
+ _updateCollectionsList() {
66
+ const collectionsContainer = this.element.querySelector(
67
+ '.messenger-help-collections'
68
+ );
69
+ const collections = this.state.helpArticles || [];
70
+ const searchQuery = (this.state.helpSearchQuery || '').toLowerCase();
71
+
72
+ // Filter collections by search
73
+ const filteredCollections = searchQuery
74
+ ? collections.filter(
75
+ (c) =>
76
+ c.title.toLowerCase().includes(searchQuery) ||
77
+ (c.description && c.description.toLowerCase().includes(searchQuery))
78
+ )
79
+ : collections;
80
+
81
+ // Update collection count
82
+ const headerEl = this.element.querySelector(
83
+ '.messenger-help-collections-header'
84
+ );
85
+ if (headerEl) {
86
+ headerEl.textContent = `${filteredCollections.length} collections`;
87
+ }
88
+
89
+ if (filteredCollections.length === 0) {
90
+ collectionsContainer.innerHTML = this._renderEmptyState();
91
+ return;
92
+ }
93
+
94
+ collectionsContainer.innerHTML = filteredCollections
95
+ .map((collection) => this._renderCollectionItem(collection))
96
+ .join('');
97
+
98
+ // Attach click events
99
+ this._attachCollectionEvents();
100
+ }
101
+
102
+ _renderCollectionItem(collection) {
103
+ const articleCount = collection.articleCount || 0;
104
+ return `
105
+ <div class="messenger-help-collection" data-collection-id="${collection.id}">
106
+ <div class="messenger-help-collection-content">
107
+ <h3 class="messenger-help-collection-title">${collection.title}</h3>
108
+ <p class="messenger-help-collection-desc">${collection.description || ''}</p>
109
+ <span class="messenger-help-collection-count">${articleCount} articles</span>
110
+ </div>
111
+ <i class="ph ph-caret-right messenger-help-collection-arrow" style="font-size: 20px;"></i>
112
+ </div>
113
+ `;
114
+ }
115
+
116
+ _renderEmptyState() {
117
+ const isSearching = this.state.helpSearchQuery;
118
+
119
+ if (isSearching) {
120
+ return `
121
+ <div class="messenger-help-empty">
122
+ <div class="messenger-help-empty-icon">
123
+ <i class="ph ph-magnifying-glass" style="font-size: 48px;"></i>
124
+ </div>
125
+ <h3>No results found</h3>
126
+ <p>Try a different search term</p>
127
+ </div>
128
+ `;
129
+ }
130
+
131
+ return `
132
+ <div class="messenger-help-empty">
133
+ <div class="messenger-help-empty-icon">
134
+ <i class="ph ph-question" style="font-size: 48px;"></i>
135
+ </div>
136
+ <h3>Help collections</h3>
137
+ <p>No collections available yet</p>
138
+ </div>
139
+ `;
140
+ }
141
+
142
+ _attachEvents() {
143
+ // Close button
144
+ this.element
145
+ .querySelector('.messenger-close-btn')
146
+ .addEventListener('click', () => {
147
+ this.state.setOpen(false);
148
+ });
149
+
150
+ // Search input
151
+ const searchInput = this.element.querySelector(
152
+ '.messenger-help-search-input'
153
+ );
154
+ let searchTimeout;
155
+ searchInput.addEventListener('input', (e) => {
156
+ clearTimeout(searchTimeout);
157
+ searchTimeout = setTimeout(() => {
158
+ this.state.setHelpSearchQuery(e.target.value);
159
+ }, 300);
160
+ });
161
+
162
+ this._attachCollectionEvents();
163
+ }
164
+
165
+ _attachCollectionEvents() {
166
+ this.element
167
+ .querySelectorAll('.messenger-help-collection')
168
+ .forEach((item) => {
169
+ item.addEventListener('click', () => {
170
+ const collectionId = item.dataset.collectionId;
171
+ const collection = this.state.helpArticles.find(
172
+ (c) => c.id === collectionId
173
+ );
174
+ if (collection && collection.url) {
175
+ window.open(collection.url, '_blank');
176
+ } else if (this.options.onArticleClick) {
177
+ this.options.onArticleClick(collection);
178
+ }
179
+ });
180
+ });
181
+ }
182
+
183
+ destroy() {
184
+ if (this._unsubscribe) {
185
+ this._unsubscribe();
186
+ }
187
+ if (this.element && this.element.parentNode) {
188
+ this.element.parentNode.removeChild(this.element);
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * HomeView - Welcome screen with team info and quick actions
3
+ */
4
+ export class HomeView {
5
+ constructor(state, options = {}) {
6
+ this.state = state;
7
+ this.options = options;
8
+ this.element = null;
9
+ this._unsubscribe = null;
10
+ }
11
+
12
+ render() {
13
+ this.element = document.createElement('div');
14
+ this.element.className = 'messenger-view messenger-home-view';
15
+
16
+ this._updateContent();
17
+
18
+ // Subscribe to state changes to re-render when data loads
19
+ this._unsubscribe = this.state.subscribe((type) => {
20
+ if (type === 'homeChangelogUpdate' || type === 'conversationsUpdate') {
21
+ this._updateContent();
22
+ }
23
+ });
24
+
25
+ return this.element;
26
+ }
27
+
28
+ _updateContent() {
29
+ const avatarsHtml = this._renderAvatarStack();
30
+ const recentChangelogHtml = this._renderRecentChangelog();
31
+
32
+ this.element.innerHTML = `
33
+ <div class="messenger-home-header">
34
+ <div class="messenger-home-header-top">
35
+ <div class="messenger-home-logo">
36
+ ${this.options.logoUrl ? `<img src="${this.options.logoUrl}" alt="${this.state.teamName}" />` : ''}
37
+ </div>
38
+ <div class="messenger-home-avatars">${avatarsHtml}</div>
39
+ <button class="messenger-close-btn" aria-label="Close">
40
+ <i class="ph ph-x" style="font-size: 20px;"></i>
41
+ </button>
42
+ </div>
43
+ <div class="messenger-home-welcome">
44
+ <span class="messenger-home-greeting">Hello there.</span>
45
+ <span class="messenger-home-question">${this.state.welcomeMessage}</span>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="messenger-home-body">
50
+ <button class="messenger-home-message-btn">
51
+ <span>Send us a message</span>
52
+ <i class="ph ph-arrow-right" style="font-size: 16px;"></i>
53
+ </button>
54
+
55
+ ${this._renderFeaturedCard()}
56
+
57
+ ${recentChangelogHtml}
58
+ </div>
59
+ `;
60
+
61
+ this._attachEvents();
62
+ }
63
+
64
+ _renderAvatarStack() {
65
+ const avatars = this.state.teamAvatars;
66
+ if (!avatars || avatars.length === 0) {
67
+ // Default avatars with initials
68
+ return `
69
+ <div class="messenger-avatar-stack">
70
+ <div class="messenger-avatar" style="background: #5856d6;">S</div>
71
+ <div class="messenger-avatar" style="background: #007aff;">T</div>
72
+ </div>
73
+ `;
74
+ }
75
+
76
+ const avatarItems = avatars
77
+ .slice(0, 3)
78
+ .map((avatar, i) => {
79
+ if (typeof avatar === 'string' && avatar.startsWith('http')) {
80
+ return `<img class="messenger-avatar" src="${avatar}" alt="Team member" style="z-index: ${3 - i};" />`;
81
+ }
82
+ return `<div class="messenger-avatar" style="background: ${this._getAvatarColor(i)}; z-index: ${3 - i};">${avatar.charAt(0).toUpperCase()}</div>`;
83
+ })
84
+ .join('');
85
+
86
+ return `<div class="messenger-avatar-stack">${avatarItems}</div>`;
87
+ }
88
+
89
+ _getAvatarColor(index) {
90
+ const colors = ['#5856d6', '#007aff', '#34c759', '#ff9500', '#ff3b30'];
91
+ return colors[index % colors.length];
92
+ }
93
+
94
+ _renderFeaturedCard() {
95
+ // Only show if there's featured content configured
96
+ if (!this.options.featuredContent) {
97
+ return '';
98
+ }
99
+
100
+ const { title, description, imageUrl, action } =
101
+ this.options.featuredContent;
102
+
103
+ return `
104
+ <div class="messenger-home-featured">
105
+ ${imageUrl ? `<img src="${imageUrl}" alt="${title}" class="messenger-home-featured-image" onerror="this.style.display='none';" />` : ''}
106
+ <div class="messenger-home-featured-content">
107
+ <h3>${title}</h3>
108
+ <p>${description}</p>
109
+ </div>
110
+ ${action ? `<button class="messenger-home-featured-btn" data-action="${action.type}" data-value="${action.value}">${action.label}</button>` : ''}
111
+ </div>
112
+ `;
113
+ }
114
+
115
+ _renderRecentChangelog() {
116
+ // Show recent changelog preview as cards with images
117
+ const changelogItems = this.state.homeChangelogItems;
118
+ if (changelogItems.length === 0) {
119
+ return '';
120
+ }
121
+
122
+ const changelogHtml = changelogItems
123
+ .map(
124
+ (item) => `
125
+ <div class="messenger-home-changelog-card" data-changelog-id="${item.id}">
126
+ ${
127
+ item.coverImage
128
+ ? `
129
+ <div class="messenger-home-changelog-cover">
130
+ <img src="${item.coverImage}" alt="${item.title}" onerror="this.style.display='none';" />
131
+ ${item.coverText ? `<span class="messenger-home-changelog-cover-text">${item.coverText}</span>` : ''}
132
+ </div>
133
+ `
134
+ : ''
135
+ }
136
+ <div class="messenger-home-changelog-card-content">
137
+ <h4 class="messenger-home-changelog-card-title">${item.title}</h4>
138
+ <p class="messenger-home-changelog-card-desc">${item.description || ''}</p>
139
+ </div>
140
+ </div>
141
+ `
142
+ )
143
+ .join('');
144
+
145
+ return `
146
+ <div class="messenger-home-changelog-section">
147
+ ${changelogHtml}
148
+ </div>
149
+ `;
150
+ }
151
+
152
+ _formatDate(dateString) {
153
+ if (!dateString) return '';
154
+ const date = new Date(dateString);
155
+ const now = new Date();
156
+ const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
157
+
158
+ if (diffDays === 0) return 'Today';
159
+ if (diffDays === 1) return 'Yesterday';
160
+ if (diffDays < 7) return `${diffDays}d ago`;
161
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
162
+ }
163
+
164
+ _attachEvents() {
165
+ // Close button
166
+ this.element
167
+ .querySelector('.messenger-close-btn')
168
+ .addEventListener('click', () => {
169
+ this.state.setOpen(false);
170
+ });
171
+
172
+ // Send message button
173
+ this.element
174
+ .querySelector('.messenger-home-message-btn')
175
+ .addEventListener('click', () => {
176
+ this.state.setView('messages');
177
+ });
178
+
179
+ // Changelog items
180
+ this.element
181
+ .querySelectorAll('.messenger-home-changelog-item')
182
+ .forEach((item) => {
183
+ item.addEventListener('click', () => {
184
+ // Navigate to changelog view with specific item selected
185
+ this.state.setView('changelog');
186
+ });
187
+ });
188
+
189
+ // See all changelog
190
+ const seeAllBtn = this.element.querySelector(
191
+ '.messenger-home-changelog-all'
192
+ );
193
+ if (seeAllBtn) {
194
+ seeAllBtn.addEventListener('click', () => {
195
+ this.state.setView('changelog');
196
+ });
197
+ }
198
+
199
+ // Featured card action
200
+ const featuredBtn = this.element.querySelector(
201
+ '.messenger-home-featured-btn'
202
+ );
203
+ if (featuredBtn) {
204
+ featuredBtn.addEventListener('click', () => {
205
+ const action = featuredBtn.dataset.action;
206
+ const value = featuredBtn.dataset.value;
207
+ if (action === 'url') {
208
+ window.open(value, '_blank');
209
+ } else if (action === 'view') {
210
+ this.state.setView(value);
211
+ }
212
+ });
213
+ }
214
+ }
215
+
216
+ destroy() {
217
+ if (this._unsubscribe) {
218
+ this._unsubscribe();
219
+ }
220
+ if (this.element && this.element.parentNode) {
221
+ this.element.parentNode.removeChild(this.element);
222
+ }
223
+ }
224
+ }
package/types/index.d.ts CHANGED
@@ -1,73 +1,135 @@
1
1
  declare module '@product7/feedback-sdk' {
2
- export interface FeedbackConfig {
3
- workspace: string;
4
- userContext?: UserContext;
5
- debug?: boolean;
6
- boardId?: string;
7
- position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
8
- theme?: 'light' | 'dark';
9
- apiUrl?: string;
10
- autoShow?: boolean;
11
- }
2
+ export interface FeedbackConfig {
3
+ workspace: string;
4
+ userContext?: UserContext;
5
+ debug?: boolean;
6
+ boardId?: string;
7
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
8
+ theme?: 'light' | 'dark';
9
+ apiUrl?: string;
10
+ autoShow?: boolean;
11
+ }
12
12
 
13
- export interface UserContext {
14
- user_id?: string;
15
- email?: string;
16
- name?: string;
17
- custom_fields?: Record<string, any>;
18
- company?: {
19
- id?: string;
20
- name?: string;
21
- monthly_spend?: number;
22
- };
23
- }
13
+ export interface UserContext {
14
+ user_id?: string;
15
+ email?: string;
16
+ name?: string;
17
+ custom_fields?: Record<string, any>;
18
+ company?: {
19
+ id?: string;
20
+ name?: string;
21
+ monthly_spend?: number;
22
+ };
23
+ }
24
24
 
25
- export interface ButtonWidget {
26
- id: string;
27
- type: 'button';
28
- mount(container?: string | HTMLElement): this;
29
- destroy(): void;
30
- show(): this;
31
- hide(): this;
32
- openModal(): void;
33
- closeModal(): void;
34
- }
25
+ export interface ButtonWidget {
26
+ id: string;
27
+ type: 'button';
28
+ mount(container?: string | HTMLElement): this;
29
+ destroy(): void;
30
+ show(): this;
31
+ hide(): this;
32
+ openModal(): void;
33
+ closeModal(): void;
34
+ }
35
35
 
36
- export class FeedbackSDK {
37
- constructor(config: FeedbackConfig);
38
- init(): Promise<{
39
- initialized: boolean;
40
- config?: any;
41
- sessionToken?: string;
42
- expiresIn?: number;
43
- }>;
44
- createWidget(
45
- type: 'button',
46
- options?: Partial<FeedbackConfig>
47
- ): ButtonWidget;
48
- setUserContext(userContext: UserContext): void;
49
- getUserContext(): UserContext | null;
50
- destroy(): void;
51
- readonly initialized: boolean;
52
- }
36
+ export class FeedbackSDK {
37
+ constructor(config: FeedbackConfig);
38
+ init(): Promise<{
39
+ initialized: boolean;
40
+ config?: any;
41
+ sessionToken?: string;
42
+ expiresIn?: number;
43
+ }>;
44
+ createWidget(
45
+ type: 'button',
46
+ options?: Partial<FeedbackConfig>
47
+ ): ButtonWidget;
48
+ setUserContext(userContext: UserContext): void;
49
+ getUserContext(): UserContext | null;
50
+ destroy(): void;
51
+ readonly initialized: boolean;
52
+ }
53
53
 
54
- interface FeedbackSDKExport {
55
- FeedbackSDK: typeof FeedbackSDK;
56
- ButtonWidget: any;
57
- create: (config: FeedbackConfig) => FeedbackSDK;
58
- initWithUser: (
59
- config: Omit<FeedbackConfig, 'userContext'>,
60
- userContext: UserContext
61
- ) => Promise<FeedbackSDK>;
62
- getInstance: () => FeedbackSDK | null;
63
- isReady: () => boolean;
64
- setUserContext: (userContext: UserContext) => void;
65
- onReady: (callback: (sdk: FeedbackSDK) => void) => void;
66
- onError: (callback: (error: Error) => void) => void;
67
- version: string;
68
- instance: FeedbackSDK | null;
69
- }
54
+ export type SurveyType = 'nps' | 'csat' | 'ces' | 'custom';
55
+
56
+ export interface SurveyQuestion {
57
+ id?: string;
58
+ label: string;
59
+ type: 'select' | 'text';
60
+ placeholder?: string;
61
+ options?: Array<{ value: string; label: string }>;
62
+ }
70
63
 
71
- const FeedbackSDKDefault: FeedbackSDKExport;
72
- export default FeedbackSDKDefault;
73
- }
64
+ export interface SurveyWidgetResponse {
65
+ type: SurveyType;
66
+ score: number | null;
67
+ feedback: string;
68
+ customAnswers: Record<string, any>;
69
+ timestamp: string;
70
+ }
71
+
72
+ export interface SurveyWidgetOptions {
73
+ surveyId?: string | null;
74
+ surveyType?: SurveyType;
75
+ position?: 'bottom-right' | 'bottom-left' | 'center' | 'bottom';
76
+ title?: string | null;
77
+ description?: string | null;
78
+ lowLabel?: string | null;
79
+ highLabel?: string | null;
80
+ customQuestions?: SurveyQuestion[];
81
+ theme?: 'light' | 'dark';
82
+ onSubmit?: (response: SurveyWidgetResponse) => void;
83
+ onDismiss?: () => void;
84
+ }
85
+
86
+ export interface SurveyWidget {
87
+ show(): this;
88
+ hide(): this;
89
+ destroy(): void;
90
+ surveyOptions: SurveyWidgetOptions;
91
+ surveyState: {
92
+ score: number | null;
93
+ feedback: string;
94
+ customAnswers: Record<string, any>;
95
+ isVisible: boolean;
96
+ };
97
+ }
98
+
99
+ export interface SurveyConfig {
100
+ workspace: string;
101
+ userContext?: UserContext;
102
+ debug?: boolean;
103
+ apiUrl?: string;
104
+ }
105
+
106
+ export class SurveySDK {
107
+ constructor(config: SurveyConfig);
108
+ init(): Promise<{ initialized: boolean }>;
109
+ createWidget(type: 'survey', options: SurveyWidgetOptions): SurveyWidget;
110
+ setUserContext(userContext: UserContext): void;
111
+ getUserContext(): UserContext | null;
112
+ destroy(): void;
113
+ readonly initialized: boolean;
114
+ }
115
+
116
+ interface FeedbackSDKExport {
117
+ FeedbackSDK: typeof FeedbackSDK;
118
+ ButtonWidget: any;
119
+ create: (config: FeedbackConfig) => FeedbackSDK;
120
+ initWithUser: (
121
+ config: Omit<FeedbackConfig, 'userContext'>,
122
+ userContext: UserContext
123
+ ) => Promise<FeedbackSDK>;
124
+ getInstance: () => FeedbackSDK | null;
125
+ isReady: () => boolean;
126
+ setUserContext: (userContext: UserContext) => void;
127
+ onReady: (callback: (sdk: FeedbackSDK) => void) => void;
128
+ onError: (callback: (error: Error) => void) => void;
129
+ version: string;
130
+ instance: FeedbackSDK | null;
131
+ }
132
+
133
+ const FeedbackSDKDefault: FeedbackSDKExport;
134
+ export default FeedbackSDKDefault;
135
+ }