@newsoftglobal/feedbackkit-jquery 1.0.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.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # @newsoftglobal/feedbackkit-jquery
2
+
3
+ jQuery SDK for [FeedbackKit](https://feedbackkit.dev) — requires jQuery 3+.
4
+
5
+ ## Installation
6
+
7
+ ### Via CDN
8
+
9
+ ```html
10
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
11
+ <script src="https://unpkg.com/@newsoftglobal/feedbackkit-jquery/dist/feedbackkit-jquery.min.js"></script>
12
+ ```
13
+
14
+ ### Via npm
15
+
16
+ ```bash
17
+ npm install @newsoftglobal/feedbackkit-jquery
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```html
23
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
24
+ <!-- Optional: for real screenshot capture -->
25
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
26
+ <!-- FeedbackKit -->
27
+ <script src="https://unpkg.com/@newsoftglobal/feedbackkit-jquery/dist/feedbackkit-jquery.min.js"></script>
28
+
29
+ <script>
30
+ $(function () {
31
+ // Initialize
32
+ $.feedbackKit({ apiKey: 'your-api-key' });
33
+
34
+ // After user login
35
+ $.feedbackKit('setUser', {
36
+ id: 'user-123',
37
+ email: 'user@example.com',
38
+ name: 'John Doe'
39
+ });
40
+
41
+ // On logout
42
+ $.feedbackKit('clearUser');
43
+ });
44
+ </script>
45
+ ```
46
+
47
+ ### Initialize with user
48
+
49
+ ```js
50
+ $.feedbackKit({
51
+ apiKey: 'your-api-key',
52
+ user: {
53
+ id: 'user-123',
54
+ email: 'user@example.com',
55
+ name: 'John'
56
+ }
57
+ });
58
+ ```
59
+
60
+ ## API
61
+
62
+ | Method | Description |
63
+ |--------|-------------|
64
+ | `$.feedbackKit({ apiKey })` | Initialize the widget |
65
+ | `$.feedbackKit('setUser', { id, email, name })` | Set user identity |
66
+ | `$.feedbackKit('clearUser')` | Clear user identity |
67
+ | `$.feedbackKit('destroy')` | Remove widget from page |
68
+
69
+ ## Features
70
+
71
+ - 🎯 Floating feedback button
72
+ - 📸 Area screenshot capture
73
+ - 📝 Feedback form (bug, feature, improvement, other)
74
+ - 📋 User feedback history
75
+ - 💬 Comment thread with admin replies
76
+ - 👤 User identification
77
+ - 🎨 Dark theme UI
78
+ - ⚡ jQuery plugin pattern
@@ -0,0 +1,555 @@
1
+ /**
2
+ * FeedbackKit jQuery SDK v1.0.0
3
+ * Requires jQuery 3+
4
+ * Usage: $.feedbackKit({ apiKey: 'your-key' })
5
+ */
6
+ (function ($) {
7
+ 'use strict';
8
+
9
+ if (!$) {
10
+ console.error('[FeedbackKit] jQuery is required');
11
+ return;
12
+ }
13
+
14
+ const SERVER_URL = 'https://backend.feedbackkit.dev';
15
+
16
+ // ==================== CSS ====================
17
+ const CSS = `
18
+ .fk-fab{position:fixed;bottom:24px;right:24px;width:52px;height:52px;border-radius:16px;border:none;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;cursor:pointer;box-shadow:0 4px 20px rgba(99,102,241,.4);display:flex;align-items:center;justify-content:center;z-index:99999;transition:all .2s}
19
+ .fk-fab:hover{transform:scale(1.06);box-shadow:0 6px 28px rgba(99,102,241,.55)}
20
+ .fk-panel{position:fixed;bottom:88px;right:24px;width:360px;max-height:480px;background:#1a1d2e;border:1px solid rgba(255,255,255,.08);border-radius:16px;box-shadow:0 16px 48px rgba(0,0,0,.5);z-index:99998;display:flex;flex-direction:column;overflow:hidden;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#f0f0f5}
21
+ .fk-panel *{box-sizing:border-box;margin:0;padding:0}
22
+ .fk-panel-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.06)}
23
+ .fk-tab{flex:1;padding:12px;background:none;border:none;color:#a0a0b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s}
24
+ .fk-tab:hover{color:#f0f0f5;background:rgba(255,255,255,.03)}
25
+ .fk-tab.active{color:#818cf8;border-bottom:2px solid #818cf8}
26
+ .fk-panel-body{padding:20px;display:flex;flex-direction:column;gap:12px}
27
+ .fk-panel-hint{font-size:13px;color:#a0a0b8;text-align:center}
28
+ .fk-btn{padding:10px 16px;border-radius:10px;border:none;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;text-align:center}
29
+ .fk-btn-primary{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;box-shadow:0 2px 12px rgba(99,102,241,.3)}
30
+ .fk-btn-primary:hover{box-shadow:0 4px 20px rgba(99,102,241,.5);transform:translateY(-1px)}
31
+ .fk-btn-full{width:100%}
32
+ .fk-btn:disabled{opacity:.5;cursor:not-allowed;transform:none!important}
33
+ .fk-form{padding:20px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;max-height:420px}
34
+ .fk-form label{font-size:12px;font-weight:600;color:#a0a0b8;display:block;margin-bottom:4px}
35
+ .fk-form input,.fk-form textarea,.fk-form select{width:100%;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:#f0f0f5;font-family:inherit;font-size:13px;outline:none;transition:border .2s}
36
+ .fk-form input:focus,.fk-form textarea:focus,.fk-form select:focus{border-color:#6366f1}
37
+ .fk-form textarea{resize:vertical;min-height:60px}
38
+ .fk-form select option{background:#1a1d2e;color:#f0f0f5}
39
+ .fk-screenshot-preview{position:relative;border-radius:8px;overflow:hidden;border:1px solid rgba(255,255,255,.08)}
40
+ .fk-screenshot-preview img{width:100%;display:block}
41
+ .fk-screenshot-remove{position:absolute;top:6px;right:6px;width:24px;height:24px;border-radius:50%;border:none;background:rgba(0,0,0,.6);color:#fff;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}
42
+ .fk-success{text-align:center;padding:30px 20px}
43
+ .fk-success-icon{font-size:40px;margin-bottom:12px}
44
+ .fk-success h4{font-size:16px;font-weight:700;margin-bottom:4px}
45
+ .fk-success p{font-size:13px;color:#a0a0b8}
46
+ .fk-list{padding:12px;overflow-y:auto;max-height:380px;display:flex;flex-direction:column;gap:6px}
47
+ .fk-list-item{padding:12px;border-radius:10px;background:rgba(255,255,255,.03);cursor:pointer;transition:background .2s}
48
+ .fk-list-item:hover{background:rgba(255,255,255,.06)}
49
+ .fk-list-item-title{font-size:13px;font-weight:600;margin-bottom:4px}
50
+ .fk-list-item-meta{display:flex;gap:8px;align-items:center}
51
+ .fk-list-item-meta span{font-size:11px;color:#a0a0b8}
52
+ .fk-badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600}
53
+ .fk-badge-bug{background:rgba(239,68,68,.12);color:#fca5a5}
54
+ .fk-badge-feature{background:rgba(99,102,241,.12);color:#818cf8}
55
+ .fk-badge-improvement{background:rgba(234,179,8,.12);color:#fde047}
56
+ .fk-badge-other{background:rgba(59,130,246,.12);color:#93c5fd}
57
+ .fk-badge-new{background:rgba(234,179,8,.12);color:#fde047}
58
+ .fk-badge-in-progress{background:rgba(59,130,246,.12);color:#93c5fd}
59
+ .fk-badge-resolved{background:rgba(34,197,94,.12);color:#86efac}
60
+ .fk-detail-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);z-index:100000;display:flex;align-items:center;justify-content:center}
61
+ .fk-detail-modal{background:#1a1d2e;border-radius:16px;width:90%;max-width:440px;max-height:80vh;border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 64px rgba(0,0,0,.5);display:flex;flex-direction:column;overflow:hidden;font-family:'Inter',-apple-system,sans-serif;color:#f0f0f5}
62
+ .fk-detail-header{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;justify-content:space-between;align-items:center}
63
+ .fk-detail-header h4{font-size:15px;font-weight:700}
64
+ .fk-detail-close{background:none;border:none;color:#a0a0b8;font-size:20px;cursor:pointer;padding:4px}
65
+ .fk-detail-body{flex:1;overflow-y:auto;padding:16px 20px}
66
+ .fk-detail-desc{font-size:13px;color:#a0a0b8;margin-bottom:16px;line-height:1.5}
67
+ .fk-comments{display:flex;flex-direction:column;gap:10px;margin-top:12px}
68
+ .fk-comment{padding:10px 14px;border-radius:10px;max-width:85%}
69
+ .fk-comment.user{background:rgba(99,102,241,.12);align-self:flex-end}
70
+ .fk-comment.admin{background:rgba(255,255,255,.06);align-self:flex-start}
71
+ .fk-comment-author{font-size:10px;font-weight:600;color:#a0a0b8;margin-bottom:4px}
72
+ .fk-comment-text{font-size:13px;line-height:1.4}
73
+ .fk-comment-time{font-size:10px;color:#555568;margin-top:4px}
74
+ .fk-detail-input{display:flex;gap:8px;padding:12px 20px;border-top:1px solid rgba(255,255,255,.06)}
75
+ .fk-detail-input input{flex:1;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:#f0f0f5;font-family:inherit;font-size:13px;outline:none}
76
+ .fk-detail-input button{padding:8px 16px;border-radius:8px;border:none;background:#6366f1;color:#fff;font-weight:600;font-size:13px;cursor:pointer}
77
+ .fk-empty{text-align:center;padding:30px 12px;color:#555568;font-size:13px}
78
+ .fk-capture-overlay{position:fixed;inset:0;z-index:100001;cursor:crosshair;background:rgba(0,0,0,.3)}
79
+ .fk-capture-selection{position:fixed;border:2px solid #6366f1;background:rgba(99,102,241,.08);z-index:100002}
80
+ .fk-capture-hint{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#1a1d2e;color:#f0f0f5;padding:10px 20px;border-radius:10px;font-size:13px;font-family:'Inter',sans-serif;z-index:100003;box-shadow:0 4px 16px rgba(0,0,0,.4)}
81
+ `;
82
+
83
+ // ==================== API ====================
84
+ class FeedbackKitAPI {
85
+ constructor(apiKey) {
86
+ this.apiKey = apiKey;
87
+ this.serverUrl = SERVER_URL;
88
+ this.user = { id: null, email: null, name: null };
89
+ }
90
+
91
+ setUser(info) {
92
+ if (!info || !info.id) return;
93
+ this.user.id = info.id;
94
+ this.user.email = info.email || null;
95
+ this.user.name = info.name || null;
96
+ }
97
+
98
+ clearUser() {
99
+ this.user = { id: null, email: null, name: null };
100
+ }
101
+
102
+ hasUser() { return !!this.user.id; }
103
+
104
+ submitFeedback(data) {
105
+ var fd = new FormData();
106
+ fd.append('title', data.title);
107
+ if (data.description) fd.append('description', data.description);
108
+ if (data.type) fd.append('type', data.type);
109
+ if (data.metadata) fd.append('metadata', JSON.stringify(data.metadata));
110
+ if (this.user.id) fd.append('userId', this.user.id);
111
+ if (this.user.name) fd.append('userName', this.user.name);
112
+ fd.append('userEmail', data.userEmail || this.user.email || '');
113
+
114
+ var self = this;
115
+ var deferred = $.Deferred();
116
+
117
+ if (data.screenshot && typeof data.screenshot === 'string' && data.screenshot.indexOf('data:') === 0) {
118
+ fetch(data.screenshot).then(function (r) { return r.blob(); }).then(function (blob) {
119
+ fd.append('screenshot', blob, 'screenshot.png');
120
+ self._send(fd, deferred);
121
+ });
122
+ } else {
123
+ if (data.screenshot) fd.append('screenshot', data.screenshot, 'screenshot.png');
124
+ self._send(fd, deferred);
125
+ }
126
+ return deferred.promise();
127
+ }
128
+
129
+ _send(fd, deferred) {
130
+ $.ajax({
131
+ url: this.serverUrl + '/api/feedbacks',
132
+ method: 'POST',
133
+ headers: { 'x-api-key': this.apiKey },
134
+ data: fd,
135
+ processData: false,
136
+ contentType: false,
137
+ success: function (res) { deferred.resolve(res); },
138
+ error: function (xhr) {
139
+ var msg = 'Error';
140
+ try { msg = JSON.parse(xhr.responseText).error || msg; } catch (e) { }
141
+ deferred.reject(msg);
142
+ }
143
+ });
144
+ }
145
+
146
+ getUserFeedbacks(page) {
147
+ if (!this.user.id) return $.Deferred().resolve({ feedbacks: [], pagination: {} }).promise();
148
+ return $.ajax({
149
+ url: this.serverUrl + '/api/feedbacks/user',
150
+ headers: { 'x-api-key': this.apiKey },
151
+ data: { userId: this.user.id, page: page || 1, limit: 20 }
152
+ });
153
+ }
154
+
155
+ getFeedbackDetail(id) {
156
+ if (!this.user.id) return $.Deferred().resolve(null).promise();
157
+ return $.ajax({
158
+ url: this.serverUrl + '/api/feedbacks/user/' + id,
159
+ headers: { 'x-api-key': this.apiKey },
160
+ data: { userId: this.user.id }
161
+ }).then(function (data) { return data.feedback; });
162
+ }
163
+
164
+ addComment(id, text) {
165
+ if (!this.user.id) return $.Deferred().reject('User not set').promise();
166
+ return $.ajax({
167
+ url: this.serverUrl + '/api/feedbacks/user/' + id + '/comments',
168
+ method: 'POST',
169
+ headers: { 'x-api-key': this.apiKey },
170
+ contentType: 'application/json',
171
+ data: JSON.stringify({ userId: this.user.id, userName: this.user.name || 'User', text: text })
172
+ });
173
+ }
174
+ }
175
+
176
+ // ==================== Widget ====================
177
+ function FeedbackKitWidget(api) {
178
+ this.api = api;
179
+ this.state = 'idle';
180
+ this.screenshot = null;
181
+ this.activeTab = 'new';
182
+ this.feedbacks = [];
183
+ this.detailFeedback = null;
184
+ this._injectCSS();
185
+ this._createFAB();
186
+ }
187
+
188
+ FeedbackKitWidget.prototype = {
189
+ _injectCSS: function () {
190
+ if ($('#fk-styles').length) return;
191
+ $('<style id="fk-styles">').text(CSS).appendTo('head');
192
+ },
193
+
194
+ _createFAB: function () {
195
+ var self = this;
196
+ this.$fab = $('<button class="fk-fab">')
197
+ .html('<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="12" y1="6" x2="12" y2="12"/></svg>')
198
+ .on('click', function () { self._togglePanel(); })
199
+ .appendTo('body');
200
+ },
201
+
202
+ _togglePanel: function () {
203
+ if (this.state === 'panel') this._closePanel();
204
+ else this._showPanel();
205
+ },
206
+
207
+ _showPanel: function () {
208
+ var self = this;
209
+ this.state = 'panel';
210
+ this._updateFABIcon('close');
211
+ $('.fk-panel').remove();
212
+
213
+ var $panel = $('<div class="fk-panel">');
214
+ var $tabs = $('<div class="fk-panel-tabs">');
215
+
216
+ $('<button class="fk-tab' + (this.activeTab === 'new' ? ' active' : '') + '">')
217
+ .text('✏️ New Feedback')
218
+ .on('click', function () { self.activeTab = 'new'; self._showPanel(); })
219
+ .appendTo($tabs);
220
+
221
+ $('<button class="fk-tab' + (this.activeTab === 'list' ? ' active' : '') + '">')
222
+ .text('📋 My Feedbacks')
223
+ .on('click', function () { self.activeTab = 'list'; self._loadFeedbacks(); self._showPanel(); })
224
+ .appendTo($tabs);
225
+
226
+ $panel.append($tabs);
227
+
228
+ if (this.activeTab === 'new') {
229
+ var $body = $('<div class="fk-panel-body">');
230
+ $body.append($('<p class="fk-panel-hint">').text('Capture a screenshot and send your feedback'));
231
+ $body.append($('<button class="fk-btn fk-btn-primary fk-btn-full">').text('📸 Select Area & Capture')
232
+ .on('click', function () { self._startCapture(); }));
233
+ $panel.append($body);
234
+ } else {
235
+ this._renderList($panel);
236
+ }
237
+
238
+ $panel.appendTo('body');
239
+ },
240
+
241
+ _closePanel: function () {
242
+ this.state = 'idle';
243
+ this._updateFABIcon('open');
244
+ $('.fk-panel').remove();
245
+ },
246
+
247
+ _updateFABIcon: function (type) {
248
+ if (type === 'close') {
249
+ this.$fab.html('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>');
250
+ } else {
251
+ this.$fab.html('<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="12" y1="6" x2="12" y2="12"/></svg>');
252
+ }
253
+ },
254
+
255
+ // ==================== Screen Capture ====================
256
+ _startCapture: function () {
257
+ var self = this;
258
+ this._closePanel();
259
+ this.state = 'capturing';
260
+ this.$fab.hide();
261
+
262
+ var $hint = $('<div class="fk-capture-hint">').text('Click and drag to select area • Esc to cancel').appendTo('body');
263
+ var $overlay = $('<div class="fk-capture-overlay">').appendTo('body');
264
+ var startX, startY, $sel;
265
+
266
+ $overlay.on('mousedown', function (e) {
267
+ startX = e.clientX;
268
+ startY = e.clientY;
269
+ $sel = $('<div class="fk-capture-selection">').css({ left: startX, top: startY }).appendTo('body');
270
+ });
271
+
272
+ $overlay.on('mousemove', function (e) {
273
+ if (!$sel) return;
274
+ $sel.css({
275
+ left: Math.min(e.clientX, startX),
276
+ top: Math.min(e.clientY, startY),
277
+ width: Math.abs(e.clientX - startX),
278
+ height: Math.abs(e.clientY - startY)
279
+ });
280
+ });
281
+
282
+ $overlay.on('mouseup', function (e) {
283
+ var rect = {
284
+ x: Math.min(e.clientX, startX), y: Math.min(e.clientY, startY),
285
+ w: Math.abs(e.clientX - startX), h: Math.abs(e.clientY - startY)
286
+ };
287
+ $('.fk-capture-overlay, .fk-capture-selection, .fk-capture-hint').remove();
288
+ self.$fab.show();
289
+ if (rect.w < 10 || rect.h < 10) { self._showPanel(); return; }
290
+ self._captureArea(rect);
291
+ });
292
+
293
+ $(document).one('keydown.fk-capture', function (e) {
294
+ if (e.key === 'Escape') {
295
+ $('.fk-capture-overlay, .fk-capture-selection, .fk-capture-hint').remove();
296
+ self.$fab.show();
297
+ self._showPanel();
298
+ }
299
+ });
300
+ },
301
+
302
+ _captureArea: function (rect) {
303
+ var self = this;
304
+ if (typeof html2canvas !== 'undefined') {
305
+ html2canvas(document.body, {
306
+ x: rect.x + window.scrollX, y: rect.y + window.scrollY,
307
+ width: rect.w, height: rect.h,
308
+ windowWidth: document.documentElement.scrollWidth,
309
+ windowHeight: document.documentElement.scrollHeight
310
+ }).then(function (canvas) {
311
+ self.screenshot = canvas.toDataURL('image/png');
312
+ self._showForm();
313
+ }).catch(function () { self._showPanel(); });
314
+ } else {
315
+ // Fallback placeholder
316
+ var c = document.createElement('canvas');
317
+ var dpr = window.devicePixelRatio || 1;
318
+ c.width = rect.w * dpr; c.height = rect.h * dpr;
319
+ var ctx = c.getContext('2d');
320
+ ctx.scale(dpr, dpr);
321
+ ctx.fillStyle = '#f0f0f5'; ctx.fillRect(0, 0, rect.w, rect.h);
322
+ ctx.font = '14px sans-serif'; ctx.fillStyle = '#555';
323
+ ctx.fillText('Screenshot ' + rect.w + '\u00d7' + rect.h, 10, 24);
324
+ ctx.fillText('Add html2canvas for real capture', 10, 44);
325
+ self.screenshot = c.toDataURL('image/png');
326
+ self._showForm();
327
+ }
328
+ },
329
+
330
+ // ==================== Feedback Form ====================
331
+ _showForm: function () {
332
+ var self = this;
333
+ this.state = 'form';
334
+ $('.fk-panel').remove();
335
+ this._updateFABIcon('close');
336
+
337
+ var $panel = $('<div class="fk-panel">');
338
+ var $form = $('<div class="fk-form">');
339
+
340
+ // Screenshot preview
341
+ if (this.screenshot) {
342
+ var $preview = $('<div class="fk-screenshot-preview">');
343
+ $preview.append($('<img>').attr('src', this.screenshot));
344
+ $preview.append($('<button class="fk-screenshot-remove">').text('\u00d7').on('click', function () {
345
+ self.screenshot = null; $preview.remove();
346
+ }));
347
+ $form.append($preview);
348
+ }
349
+
350
+ // Fields
351
+ var $title = $('<input type="text" placeholder="Brief summary...">');
352
+ $form.append($('<div>').append($('<label>').text('Title *')).append($title));
353
+
354
+ var $type = $('<select>');
355
+ $.each([
356
+ ['bug', '🐛 Bug'], ['feature', '💡 Feature'],
357
+ ['improvement', '⚡ Improvement'], ['other', '📝 Other']
358
+ ], function (i, t) { $type.append($('<option>').val(t[0]).text(t[1])); });
359
+ $form.append($('<div>').append($('<label>').text('Type')).append($type));
360
+
361
+ var $desc = $('<textarea rows="3" placeholder="More details...">');
362
+ $form.append($('<div>').append($('<label>').text('Description')).append($desc));
363
+
364
+ var $email = $('<input type="email" placeholder="your@email.com">');
365
+ if (this.api.user.email) $email.val(this.api.user.email);
366
+ $form.append($('<div>').append($('<label>').text('Email (optional)')).append($email));
367
+
368
+ // Actions
369
+ var $actions = $('<div>').css({ display: 'flex', gap: '8px', marginTop: '4px' });
370
+
371
+ $actions.append($('<button class="fk-btn">').text('Cancel')
372
+ .css({ flex: 1, background: 'rgba(255,255,255,.06)', color: '#a0a0b8' })
373
+ .on('click', function () { self.screenshot = null; self._showPanel(); }));
374
+
375
+ var $submit = $('<button class="fk-btn fk-btn-primary">').text('Submit').css('flex', '1');
376
+ $submit.on('click', function () {
377
+ if (!$title.val().trim()) { $title.css('border-color', '#ef4444'); return; }
378
+ $submit.prop('disabled', true).text('Sending...');
379
+ self.api.submitFeedback({
380
+ title: $title.val().trim(),
381
+ description: $desc.val().trim(),
382
+ type: $type.val(),
383
+ screenshot: self.screenshot,
384
+ userEmail: $email.val().trim(),
385
+ metadata: { url: window.location.href, userAgent: navigator.userAgent }
386
+ }).done(function () {
387
+ self.screenshot = null;
388
+ self._showSuccess();
389
+ }).fail(function (err) {
390
+ $submit.prop('disabled', false).text('Submit');
391
+ alert('Failed: ' + (typeof err === 'string' ? err : 'Unknown error'));
392
+ });
393
+ });
394
+ $actions.append($submit);
395
+ $form.append($actions);
396
+ $panel.append($form).appendTo('body');
397
+ },
398
+
399
+ _showSuccess: function () {
400
+ var self = this;
401
+ $('.fk-panel').remove();
402
+ var $panel = $('<div class="fk-panel">');
403
+ $panel.append($('<div class="fk-success">').html(
404
+ '<div class="fk-success-icon">✅</div><h4>Thank you!</h4><p>Your feedback has been submitted successfully.</p>'
405
+ ));
406
+ $panel.appendTo('body');
407
+ this.state = 'panel';
408
+ setTimeout(function () { self.activeTab = 'list'; self._loadFeedbacks(); self._showPanel(); }, 2000);
409
+ },
410
+
411
+ // ==================== Feedback List ====================
412
+ _loadFeedbacks: function () {
413
+ var self = this;
414
+ this.api.getUserFeedbacks().done(function (data) {
415
+ self.feedbacks = data.feedbacks || [];
416
+ }).fail(function () { self.feedbacks = []; });
417
+ },
418
+
419
+ _renderList: function ($panel) {
420
+ var self = this;
421
+ var $list = $('<div class="fk-list">');
422
+
423
+ if (!this.api.hasUser()) {
424
+ $list.html('<div class="fk-empty">Login to view your feedbacks</div>');
425
+ $panel.append($list);
426
+ return;
427
+ }
428
+ if (!this.feedbacks.length) {
429
+ $list.html('<div class="fk-empty">No feedbacks yet</div>');
430
+ $panel.append($list);
431
+ return;
432
+ }
433
+
434
+ $.each(this.feedbacks, function (i, fb) {
435
+ var $item = $('<div class="fk-list-item">');
436
+ $item.append($('<div class="fk-list-item-title">').text(fb.title));
437
+ var $meta = $('<div class="fk-list-item-meta">');
438
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.type || 'other') + '">').text(fb.type || 'other'));
439
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.status || 'new').replace(' ', '-') + '">').text(fb.status || 'new'));
440
+ $meta.append($('<span>').text(new Date(fb.createdAt).toLocaleDateString()));
441
+ $item.append($meta);
442
+ $item.on('click', function () { self._showDetail(fb._id); });
443
+ $list.append($item);
444
+ });
445
+
446
+ $panel.append($list);
447
+ },
448
+
449
+ // ==================== Feedback Detail ====================
450
+ _showDetail: function (id) {
451
+ var self = this;
452
+ this.api.getFeedbackDetail(id).done(function (fb) {
453
+ if (!fb) return;
454
+ self.detailFeedback = fb;
455
+ self._renderDetail();
456
+ });
457
+ },
458
+
459
+ _renderDetail: function () {
460
+ var self = this;
461
+ var fb = this.detailFeedback;
462
+ $('.fk-detail-overlay').remove();
463
+
464
+ var $overlay = $('<div class="fk-detail-overlay">');
465
+ var $modal = $('<div class="fk-detail-modal">');
466
+
467
+ // Header
468
+ var $header = $('<div class="fk-detail-header">');
469
+ $header.append($('<h4>').text(fb.title));
470
+ $header.append($('<button class="fk-detail-close">').text('\u00d7').on('click', function () { $overlay.remove(); }));
471
+ $modal.append($header);
472
+
473
+ // Body
474
+ var $body = $('<div class="fk-detail-body">');
475
+ if (fb.description) $body.append($('<p class="fk-detail-desc">').text(fb.description));
476
+
477
+ var $meta = $('<div>').css({ display: 'flex', gap: '8px', marginBottom: '16px' });
478
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.type || 'other') + '">').text(fb.type || 'other'));
479
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.status || 'new').replace(' ', '-') + '">').text(fb.status || 'new'));
480
+ $body.append($meta);
481
+
482
+ // Comments
483
+ if (fb.comments && fb.comments.length) {
484
+ $body.append($('<label>').css({ fontSize: '12px', fontWeight: 600, color: '#a0a0b8', marginBottom: '8px', display: 'block' }).text('Conversation'));
485
+ var $comments = $('<div class="fk-comments">');
486
+ $.each(fb.comments, function (i, c) {
487
+ var $c = $('<div class="fk-comment ' + c.role + '">');
488
+ $c.append($('<div class="fk-comment-author">').text(c.authorName || c.role));
489
+ $c.append($('<div class="fk-comment-text">').text(c.text));
490
+ $c.append($('<div class="fk-comment-time">').text(new Date(c.createdAt).toLocaleString()));
491
+ $comments.append($c);
492
+ });
493
+ $body.append($comments);
494
+ }
495
+ $modal.append($body);
496
+
497
+ // Comment input
498
+ var $inputDiv = $('<div class="fk-detail-input">');
499
+ var $input = $('<input placeholder="Write a reply...">');
500
+ var $sendBtn = $('<button>').text('Send');
501
+ $sendBtn.on('click', function () {
502
+ if (!$input.val().trim()) return;
503
+ $sendBtn.prop('disabled', true);
504
+ self.api.addComment(fb._id, $input.val().trim()).done(function () {
505
+ $input.val('');
506
+ self.api.getFeedbackDetail(fb._id).done(function (updated) {
507
+ self.detailFeedback = updated;
508
+ self._renderDetail();
509
+ });
510
+ }).fail(function () {
511
+ alert('Failed to send comment');
512
+ }).always(function () { $sendBtn.prop('disabled', false); });
513
+ });
514
+ $input.on('keydown', function (e) { if (e.key === 'Enter') $sendBtn.click(); });
515
+ $inputDiv.append($input, $sendBtn);
516
+ $modal.append($inputDiv);
517
+
518
+ $overlay.append($modal);
519
+ $overlay.on('click', function (e) { if (e.target === $overlay[0]) $overlay.remove(); });
520
+ $overlay.appendTo('body');
521
+ },
522
+
523
+ destroy: function () {
524
+ this.$fab.remove();
525
+ $('.fk-panel, .fk-detail-overlay, .fk-capture-overlay, .fk-capture-selection, .fk-capture-hint, #fk-styles').remove();
526
+ }
527
+ };
528
+
529
+ // ==================== jQuery Plugin ====================
530
+ var _instance = null;
531
+
532
+ $.feedbackKit = function (options) {
533
+ if (typeof options === 'string') {
534
+ // Method calls: $.feedbackKit('setUser', {...})
535
+ if (options === 'setUser' && _instance) _instance.api.setUser(arguments[1]);
536
+ else if (options === 'clearUser' && _instance) _instance.api.clearUser();
537
+ else if (options === 'destroy' && _instance) { _instance.widget.destroy(); _instance = null; }
538
+ return;
539
+ }
540
+
541
+ if (!options || !options.apiKey) {
542
+ console.warn('[FeedbackKit] Missing apiKey');
543
+ return;
544
+ }
545
+
546
+ var api = new FeedbackKitAPI(options.apiKey);
547
+ var widget = new FeedbackKitWidget(api);
548
+
549
+ if (options.user) api.setUser(options.user);
550
+
551
+ _instance = { api: api, widget: widget };
552
+ return _instance;
553
+ };
554
+
555
+ })(jQuery);
@@ -0,0 +1,467 @@
1
+ (function ($) {
2
+ 'use strict';
3
+ if (!$) {
4
+ console.error('[FeedbackKit] jQuery is required');
5
+ return;
6
+ }
7
+ const SERVER_URL = 'https://backend.feedbackkit.dev';
8
+ const CSS = `
9
+ .fk-fab{position:fixed;bottom:24px;right:24px;width:52px;height:52px;border-radius:16px;border:none;background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;cursor:pointer;box-shadow:0 4px 20px rgba(99,102,241,.4);display:flex;align-items:center;justify-content:center;z-index:99999;transition:all .2s}
10
+ .fk-fab:hover{transform:scale(1.06);box-shadow:0 6px 28px rgba(99,102,241,.55)}
11
+ .fk-panel{position:fixed;bottom:88px;right:24px;width:360px;max-height:480px;background:#1a1d2e;border:1px solid rgba(255,255,255,.08);border-radius:16px;box-shadow:0 16px 48px rgba(0,0,0,.5);z-index:99998;display:flex;flex-direction:column;overflow:hidden;font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:#f0f0f5}
12
+ .fk-panel *{box-sizing:border-box;margin:0;padding:0}
13
+ .fk-panel-tabs{display:flex;border-bottom:1px solid rgba(255,255,255,.06)}
14
+ .fk-tab{flex:1;padding:12px;background:none;border:none;color:#a0a0b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;transition:all .2s}
15
+ .fk-tab:hover{color:#f0f0f5;background:rgba(255,255,255,.03)}
16
+ .fk-tab.active{color:#818cf8;border-bottom:2px solid #818cf8}
17
+ .fk-panel-body{padding:20px;display:flex;flex-direction:column;gap:12px}
18
+ .fk-panel-hint{font-size:13px;color:#a0a0b8;text-align:center}
19
+ .fk-btn{padding:10px 16px;border-radius:10px;border:none;font-family:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s;text-align:center}
20
+ .fk-btn-primary{background:linear-gradient(135deg,#6366f1,#8b5cf6);color:#fff;box-shadow:0 2px 12px rgba(99,102,241,.3)}
21
+ .fk-btn-primary:hover{box-shadow:0 4px 20px rgba(99,102,241,.5);transform:translateY(-1px)}
22
+ .fk-btn-full{width:100%}
23
+ .fk-btn:disabled{opacity:.5;cursor:not-allowed;transform:none!important}
24
+ .fk-form{padding:20px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;max-height:420px}
25
+ .fk-form label{font-size:12px;font-weight:600;color:#a0a0b8;display:block;margin-bottom:4px}
26
+ .fk-form input,.fk-form textarea,.fk-form select{width:100%;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:#f0f0f5;font-family:inherit;font-size:13px;outline:none;transition:border .2s}
27
+ .fk-form input:focus,.fk-form textarea:focus,.fk-form select:focus{border-color:#6366f1}
28
+ .fk-form textarea{resize:vertical;min-height:60px}
29
+ .fk-form select option{background:#1a1d2e;color:#f0f0f5}
30
+ .fk-screenshot-preview{position:relative;border-radius:8px;overflow:hidden;border:1px solid rgba(255,255,255,.08)}
31
+ .fk-screenshot-preview img{width:100%;display:block}
32
+ .fk-screenshot-remove{position:absolute;top:6px;right:6px;width:24px;height:24px;border-radius:50%;border:none;background:rgba(0,0,0,.6);color:#fff;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center}
33
+ .fk-success{text-align:center;padding:30px 20px}
34
+ .fk-success-icon{font-size:40px;margin-bottom:12px}
35
+ .fk-success h4{font-size:16px;font-weight:700;margin-bottom:4px}
36
+ .fk-success p{font-size:13px;color:#a0a0b8}
37
+ .fk-list{padding:12px;overflow-y:auto;max-height:380px;display:flex;flex-direction:column;gap:6px}
38
+ .fk-list-item{padding:12px;border-radius:10px;background:rgba(255,255,255,.03);cursor:pointer;transition:background .2s}
39
+ .fk-list-item:hover{background:rgba(255,255,255,.06)}
40
+ .fk-list-item-title{font-size:13px;font-weight:600;margin-bottom:4px}
41
+ .fk-list-item-meta{display:flex;gap:8px;align-items:center}
42
+ .fk-list-item-meta span{font-size:11px;color:#a0a0b8}
43
+ .fk-badge{display:inline-block;padding:2px 8px;border-radius:12px;font-size:10px;font-weight:600}
44
+ .fk-badge-bug{background:rgba(239,68,68,.12);color:#fca5a5}
45
+ .fk-badge-feature{background:rgba(99,102,241,.12);color:#818cf8}
46
+ .fk-badge-improvement{background:rgba(234,179,8,.12);color:#fde047}
47
+ .fk-badge-other{background:rgba(59,130,246,.12);color:#93c5fd}
48
+ .fk-badge-new{background:rgba(234,179,8,.12);color:#fde047}
49
+ .fk-badge-in-progress{background:rgba(59,130,246,.12);color:#93c5fd}
50
+ .fk-badge-resolved{background:rgba(34,197,94,.12);color:#86efac}
51
+ .fk-detail-overlay{position:fixed;inset:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);z-index:100000;display:flex;align-items:center;justify-content:center}
52
+ .fk-detail-modal{background:#1a1d2e;border-radius:16px;width:90%;max-width:440px;max-height:80vh;border:1px solid rgba(255,255,255,.08);box-shadow:0 24px 64px rgba(0,0,0,.5);display:flex;flex-direction:column;overflow:hidden;font-family:'Inter',-apple-system,sans-serif;color:#f0f0f5}
53
+ .fk-detail-header{padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.06);display:flex;justify-content:space-between;align-items:center}
54
+ .fk-detail-header h4{font-size:15px;font-weight:700}
55
+ .fk-detail-close{background:none;border:none;color:#a0a0b8;font-size:20px;cursor:pointer;padding:4px}
56
+ .fk-detail-body{flex:1;overflow-y:auto;padding:16px 20px}
57
+ .fk-detail-desc{font-size:13px;color:#a0a0b8;margin-bottom:16px;line-height:1.5}
58
+ .fk-comments{display:flex;flex-direction:column;gap:10px;margin-top:12px}
59
+ .fk-comment{padding:10px 14px;border-radius:10px;max-width:85%}
60
+ .fk-comment.user{background:rgba(99,102,241,.12);align-self:flex-end}
61
+ .fk-comment.admin{background:rgba(255,255,255,.06);align-self:flex-start}
62
+ .fk-comment-author{font-size:10px;font-weight:600;color:#a0a0b8;margin-bottom:4px}
63
+ .fk-comment-text{font-size:13px;line-height:1.4}
64
+ .fk-comment-time{font-size:10px;color:#555568;margin-top:4px}
65
+ .fk-detail-input{display:flex;gap:8px;padding:12px 20px;border-top:1px solid rgba(255,255,255,.06)}
66
+ .fk-detail-input input{flex:1;padding:10px 12px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);color:#f0f0f5;font-family:inherit;font-size:13px;outline:none}
67
+ .fk-detail-input button{padding:8px 16px;border-radius:8px;border:none;background:#6366f1;color:#fff;font-weight:600;font-size:13px;cursor:pointer}
68
+ .fk-empty{text-align:center;padding:30px 12px;color:#555568;font-size:13px}
69
+ .fk-capture-overlay{position:fixed;inset:0;z-index:100001;cursor:crosshair;background:rgba(0,0,0,.3)}
70
+ .fk-capture-selection{position:fixed;border:2px solid #6366f1;background:rgba(99,102,241,.08);z-index:100002}
71
+ .fk-capture-hint{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#1a1d2e;color:#f0f0f5;padding:10px 20px;border-radius:10px;font-size:13px;font-family:'Inter',sans-serif;z-index:100003;box-shadow:0 4px 16px rgba(0,0,0,.4)}
72
+ `;
73
+ class FeedbackKitAPI {
74
+ constructor(apiKey) {
75
+ this.apiKey = apiKey;
76
+ this.serverUrl = SERVER_URL;
77
+ this.user = { id: null, email: null, name: null };
78
+ }
79
+ setUser(info) {
80
+ if (!info || !info.id) return;
81
+ this.user.id = info.id;
82
+ this.user.email = info.email || null;
83
+ this.user.name = info.name || null;
84
+ }
85
+ clearUser() {
86
+ this.user = { id: null, email: null, name: null };
87
+ }
88
+ hasUser() { return !!this.user.id; }
89
+ submitFeedback(data) {
90
+ var fd = new FormData();
91
+ fd.append('title', data.title);
92
+ if (data.description) fd.append('description', data.description);
93
+ if (data.type) fd.append('type', data.type);
94
+ if (data.metadata) fd.append('metadata', JSON.stringify(data.metadata));
95
+ if (this.user.id) fd.append('userId', this.user.id);
96
+ if (this.user.name) fd.append('userName', this.user.name);
97
+ fd.append('userEmail', data.userEmail || this.user.email || '');
98
+ var self = this;
99
+ var deferred = $.Deferred();
100
+ if (data.screenshot && typeof data.screenshot === 'string' && data.screenshot.indexOf('data:') === 0) {
101
+ fetch(data.screenshot).then(function (r) { return r.blob(); }).then(function (blob) {
102
+ fd.append('screenshot', blob, 'screenshot.png');
103
+ self._send(fd, deferred);
104
+ });
105
+ } else {
106
+ if (data.screenshot) fd.append('screenshot', data.screenshot, 'screenshot.png');
107
+ self._send(fd, deferred);
108
+ }
109
+ return deferred.promise();
110
+ }
111
+ _send(fd, deferred) {
112
+ $.ajax({
113
+ url: this.serverUrl + '/api/feedbacks',
114
+ method: 'POST',
115
+ headers: { 'x-api-key': this.apiKey },
116
+ data: fd,
117
+ processData: false,
118
+ contentType: false,
119
+ success: function (res) { deferred.resolve(res); },
120
+ error: function (xhr) {
121
+ var msg = 'Error';
122
+ try { msg = JSON.parse(xhr.responseText).error || msg; } catch (e) { }
123
+ deferred.reject(msg);
124
+ }
125
+ });
126
+ }
127
+ getUserFeedbacks(page) {
128
+ if (!this.user.id) return $.Deferred().resolve({ feedbacks: [], pagination: {} }).promise();
129
+ return $.ajax({
130
+ url: this.serverUrl + '/api/feedbacks/user',
131
+ headers: { 'x-api-key': this.apiKey },
132
+ data: { userId: this.user.id, page: page || 1, limit: 20 }
133
+ });
134
+ }
135
+ getFeedbackDetail(id) {
136
+ if (!this.user.id) return $.Deferred().resolve(null).promise();
137
+ return $.ajax({
138
+ url: this.serverUrl + '/api/feedbacks/user/' + id,
139
+ headers: { 'x-api-key': this.apiKey },
140
+ data: { userId: this.user.id }
141
+ }).then(function (data) { return data.feedback; });
142
+ }
143
+ addComment(id, text) {
144
+ if (!this.user.id) return $.Deferred().reject('User not set').promise();
145
+ return $.ajax({
146
+ url: this.serverUrl + '/api/feedbacks/user/' + id + '/comments',
147
+ method: 'POST',
148
+ headers: { 'x-api-key': this.apiKey },
149
+ contentType: 'application/json',
150
+ data: JSON.stringify({ userId: this.user.id, userName: this.user.name || 'User', text: text })
151
+ });
152
+ }
153
+ }
154
+ function FeedbackKitWidget(api) {
155
+ this.api = api;
156
+ this.state = 'idle';
157
+ this.screenshot = null;
158
+ this.activeTab = 'new';
159
+ this.feedbacks = [];
160
+ this.detailFeedback = null;
161
+ this._injectCSS();
162
+ this._createFAB();
163
+ }
164
+ FeedbackKitWidget.prototype = {
165
+ _injectCSS: function () {
166
+ if ($('#fk-styles').length) return;
167
+ $('<style id="fk-styles">').text(CSS).appendTo('head');
168
+ },
169
+ _createFAB: function () {
170
+ var self = this;
171
+ this.$fab = $('<button class="fk-fab">')
172
+ .html('<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="12" y1="6" x2="12" y2="12"/></svg>')
173
+ .on('click', function () { self._togglePanel(); })
174
+ .appendTo('body');
175
+ },
176
+ _togglePanel: function () {
177
+ if (this.state === 'panel') this._closePanel();
178
+ else this._showPanel();
179
+ },
180
+ _showPanel: function () {
181
+ var self = this;
182
+ this.state = 'panel';
183
+ this._updateFABIcon('close');
184
+ $('.fk-panel').remove();
185
+ var $panel = $('<div class="fk-panel">');
186
+ var $tabs = $('<div class="fk-panel-tabs">');
187
+ $('<button class="fk-tab' + (this.activeTab === 'new' ? ' active' : '') + '">')
188
+ .text('✏️ New Feedback')
189
+ .on('click', function () { self.activeTab = 'new'; self._showPanel(); })
190
+ .appendTo($tabs);
191
+ $('<button class="fk-tab' + (this.activeTab === 'list' ? ' active' : '') + '">')
192
+ .text('📋 My Feedbacks')
193
+ .on('click', function () { self.activeTab = 'list'; self._loadFeedbacks(); self._showPanel(); })
194
+ .appendTo($tabs);
195
+ $panel.append($tabs);
196
+ if (this.activeTab === 'new') {
197
+ var $body = $('<div class="fk-panel-body">');
198
+ $body.append($('<p class="fk-panel-hint">').text('Capture a screenshot and send your feedback'));
199
+ $body.append($('<button class="fk-btn fk-btn-primary fk-btn-full">').text('📸 Select Area & Capture')
200
+ .on('click', function () { self._startCapture(); }));
201
+ $panel.append($body);
202
+ } else {
203
+ this._renderList($panel);
204
+ }
205
+ $panel.appendTo('body');
206
+ },
207
+ _closePanel: function () {
208
+ this.state = 'idle';
209
+ this._updateFABIcon('open');
210
+ $('.fk-panel').remove();
211
+ },
212
+ _updateFABIcon: function (type) {
213
+ if (type === 'close') {
214
+ this.$fab.html('<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>');
215
+ } else {
216
+ this.$fab.html('<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="9" x2="15" y2="9"/><line x1="12" y1="6" x2="12" y2="12"/></svg>');
217
+ }
218
+ },
219
+ _startCapture: function () {
220
+ var self = this;
221
+ this._closePanel();
222
+ this.state = 'capturing';
223
+ this.$fab.hide();
224
+ var $hint = $('<div class="fk-capture-hint">').text('Click and drag to select area • Esc to cancel').appendTo('body');
225
+ var $overlay = $('<div class="fk-capture-overlay">').appendTo('body');
226
+ var startX, startY, $sel;
227
+ $overlay.on('mousedown', function (e) {
228
+ startX = e.clientX;
229
+ startY = e.clientY;
230
+ $sel = $('<div class="fk-capture-selection">').css({ left: startX, top: startY }).appendTo('body');
231
+ });
232
+ $overlay.on('mousemove', function (e) {
233
+ if (!$sel) return;
234
+ $sel.css({
235
+ left: Math.min(e.clientX, startX),
236
+ top: Math.min(e.clientY, startY),
237
+ width: Math.abs(e.clientX - startX),
238
+ height: Math.abs(e.clientY - startY)
239
+ });
240
+ });
241
+ $overlay.on('mouseup', function (e) {
242
+ var rect = {
243
+ x: Math.min(e.clientX, startX), y: Math.min(e.clientY, startY),
244
+ w: Math.abs(e.clientX - startX), h: Math.abs(e.clientY - startY)
245
+ };
246
+ $('.fk-capture-overlay, .fk-capture-selection, .fk-capture-hint').remove();
247
+ self.$fab.show();
248
+ if (rect.w < 10 || rect.h < 10) { self._showPanel(); return; }
249
+ self._captureArea(rect);
250
+ });
251
+ $(document).one('keydown.fk-capture', function (e) {
252
+ if (e.key === 'Escape') {
253
+ $('.fk-capture-overlay, .fk-capture-selection, .fk-capture-hint').remove();
254
+ self.$fab.show();
255
+ self._showPanel();
256
+ }
257
+ });
258
+ },
259
+ _captureArea: function (rect) {
260
+ var self = this;
261
+ if (typeof html2canvas !== 'undefined') {
262
+ html2canvas(document.body, {
263
+ x: rect.x + window.scrollX, y: rect.y + window.scrollY,
264
+ width: rect.w, height: rect.h,
265
+ windowWidth: document.documentElement.scrollWidth,
266
+ windowHeight: document.documentElement.scrollHeight
267
+ }).then(function (canvas) {
268
+ self.screenshot = canvas.toDataURL('image/png');
269
+ self._showForm();
270
+ }).catch(function () { self._showPanel(); });
271
+ } else {
272
+ var c = document.createElement('canvas');
273
+ var dpr = window.devicePixelRatio || 1;
274
+ c.width = rect.w * dpr; c.height = rect.h * dpr;
275
+ var ctx = c.getContext('2d');
276
+ ctx.scale(dpr, dpr);
277
+ ctx.fillStyle = '#f0f0f5'; ctx.fillRect(0, 0, rect.w, rect.h);
278
+ ctx.font = '14px sans-serif'; ctx.fillStyle = '#555';
279
+ ctx.fillText('Screenshot ' + rect.w + '\u00d7' + rect.h, 10, 24);
280
+ ctx.fillText('Add html2canvas for real capture', 10, 44);
281
+ self.screenshot = c.toDataURL('image/png');
282
+ self._showForm();
283
+ }
284
+ },
285
+ _showForm: function () {
286
+ var self = this;
287
+ this.state = 'form';
288
+ $('.fk-panel').remove();
289
+ this._updateFABIcon('close');
290
+ var $panel = $('<div class="fk-panel">');
291
+ var $form = $('<div class="fk-form">');
292
+ if (this.screenshot) {
293
+ var $preview = $('<div class="fk-screenshot-preview">');
294
+ $preview.append($('<img>').attr('src', this.screenshot));
295
+ $preview.append($('<button class="fk-screenshot-remove">').text('\u00d7').on('click', function () {
296
+ self.screenshot = null; $preview.remove();
297
+ }));
298
+ $form.append($preview);
299
+ }
300
+ var $title = $('<input type="text" placeholder="Brief summary...">');
301
+ $form.append($('<div>').append($('<label>').text('Title *')).append($title));
302
+ var $type = $('<select>');
303
+ $.each([
304
+ ['bug', '🐛 Bug'], ['feature', '💡 Feature'],
305
+ ['improvement', '⚡ Improvement'], ['other', '📝 Other']
306
+ ], function (i, t) { $type.append($('<option>').val(t[0]).text(t[1])); });
307
+ $form.append($('<div>').append($('<label>').text('Type')).append($type));
308
+ var $desc = $('<textarea rows="3" placeholder="More details...">');
309
+ $form.append($('<div>').append($('<label>').text('Description')).append($desc));
310
+ var $email = $('<input type="email" placeholder="your@email.com">');
311
+ if (this.api.user.email) $email.val(this.api.user.email);
312
+ $form.append($('<div>').append($('<label>').text('Email (optional)')).append($email));
313
+ var $actions = $('<div>').css({ display: 'flex', gap: '8px', marginTop: '4px' });
314
+ $actions.append($('<button class="fk-btn">').text('Cancel')
315
+ .css({ flex: 1, background: 'rgba(255,255,255,.06)', color: '#a0a0b8' })
316
+ .on('click', function () { self.screenshot = null; self._showPanel(); }));
317
+ var $submit = $('<button class="fk-btn fk-btn-primary">').text('Submit').css('flex', '1');
318
+ $submit.on('click', function () {
319
+ if (!$title.val().trim()) { $title.css('border-color', '#ef4444'); return; }
320
+ $submit.prop('disabled', true).text('Sending...');
321
+ self.api.submitFeedback({
322
+ title: $title.val().trim(),
323
+ description: $desc.val().trim(),
324
+ type: $type.val(),
325
+ screenshot: self.screenshot,
326
+ userEmail: $email.val().trim(),
327
+ metadata: { url: window.location.href, userAgent: navigator.userAgent }
328
+ }).done(function () {
329
+ self.screenshot = null;
330
+ self._showSuccess();
331
+ }).fail(function (err) {
332
+ $submit.prop('disabled', false).text('Submit');
333
+ alert('Failed: ' + (typeof err === 'string' ? err : 'Unknown error'));
334
+ });
335
+ });
336
+ $actions.append($submit);
337
+ $form.append($actions);
338
+ $panel.append($form).appendTo('body');
339
+ },
340
+ _showSuccess: function () {
341
+ var self = this;
342
+ $('.fk-panel').remove();
343
+ var $panel = $('<div class="fk-panel">');
344
+ $panel.append($('<div class="fk-success">').html(
345
+ '<div class="fk-success-icon">✅</div><h4>Thank you!</h4><p>Your feedback has been submitted successfully.</p>'
346
+ ));
347
+ $panel.appendTo('body');
348
+ this.state = 'panel';
349
+ setTimeout(function () { self.activeTab = 'list'; self._loadFeedbacks(); self._showPanel(); }, 2000);
350
+ },
351
+ _loadFeedbacks: function () {
352
+ var self = this;
353
+ this.api.getUserFeedbacks().done(function (data) {
354
+ self.feedbacks = data.feedbacks || [];
355
+ }).fail(function () { self.feedbacks = []; });
356
+ },
357
+ _renderList: function ($panel) {
358
+ var self = this;
359
+ var $list = $('<div class="fk-list">');
360
+ if (!this.api.hasUser()) {
361
+ $list.html('<div class="fk-empty">Login to view your feedbacks</div>');
362
+ $panel.append($list);
363
+ return;
364
+ }
365
+ if (!this.feedbacks.length) {
366
+ $list.html('<div class="fk-empty">No feedbacks yet</div>');
367
+ $panel.append($list);
368
+ return;
369
+ }
370
+ $.each(this.feedbacks, function (i, fb) {
371
+ var $item = $('<div class="fk-list-item">');
372
+ $item.append($('<div class="fk-list-item-title">').text(fb.title));
373
+ var $meta = $('<div class="fk-list-item-meta">');
374
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.type || 'other') + '">').text(fb.type || 'other'));
375
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.status || 'new').replace(' ', '-') + '">').text(fb.status || 'new'));
376
+ $meta.append($('<span>').text(new Date(fb.createdAt).toLocaleDateString()));
377
+ $item.append($meta);
378
+ $item.on('click', function () { self._showDetail(fb._id); });
379
+ $list.append($item);
380
+ });
381
+ $panel.append($list);
382
+ },
383
+ _showDetail: function (id) {
384
+ var self = this;
385
+ this.api.getFeedbackDetail(id).done(function (fb) {
386
+ if (!fb) return;
387
+ self.detailFeedback = fb;
388
+ self._renderDetail();
389
+ });
390
+ },
391
+ _renderDetail: function () {
392
+ var self = this;
393
+ var fb = this.detailFeedback;
394
+ $('.fk-detail-overlay').remove();
395
+ var $overlay = $('<div class="fk-detail-overlay">');
396
+ var $modal = $('<div class="fk-detail-modal">');
397
+ var $header = $('<div class="fk-detail-header">');
398
+ $header.append($('<h4>').text(fb.title));
399
+ $header.append($('<button class="fk-detail-close">').text('\u00d7').on('click', function () { $overlay.remove(); }));
400
+ $modal.append($header);
401
+ var $body = $('<div class="fk-detail-body">');
402
+ if (fb.description) $body.append($('<p class="fk-detail-desc">').text(fb.description));
403
+ var $meta = $('<div>').css({ display: 'flex', gap: '8px', marginBottom: '16px' });
404
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.type || 'other') + '">').text(fb.type || 'other'));
405
+ $meta.append($('<span class="fk-badge fk-badge-' + (fb.status || 'new').replace(' ', '-') + '">').text(fb.status || 'new'));
406
+ $body.append($meta);
407
+ if (fb.comments && fb.comments.length) {
408
+ $body.append($('<label>').css({ fontSize: '12px', fontWeight: 600, color: '#a0a0b8', marginBottom: '8px', display: 'block' }).text('Conversation'));
409
+ var $comments = $('<div class="fk-comments">');
410
+ $.each(fb.comments, function (i, c) {
411
+ var $c = $('<div class="fk-comment ' + c.role + '">');
412
+ $c.append($('<div class="fk-comment-author">').text(c.authorName || c.role));
413
+ $c.append($('<div class="fk-comment-text">').text(c.text));
414
+ $c.append($('<div class="fk-comment-time">').text(new Date(c.createdAt).toLocaleString()));
415
+ $comments.append($c);
416
+ });
417
+ $body.append($comments);
418
+ }
419
+ $modal.append($body);
420
+ var $inputDiv = $('<div class="fk-detail-input">');
421
+ var $input = $('<input placeholder="Write a reply...">');
422
+ var $sendBtn = $('<button>').text('Send');
423
+ $sendBtn.on('click', function () {
424
+ if (!$input.val().trim()) return;
425
+ $sendBtn.prop('disabled', true);
426
+ self.api.addComment(fb._id, $input.val().trim()).done(function () {
427
+ $input.val('');
428
+ self.api.getFeedbackDetail(fb._id).done(function (updated) {
429
+ self.detailFeedback = updated;
430
+ self._renderDetail();
431
+ });
432
+ }).fail(function () {
433
+ alert('Failed to send comment');
434
+ }).always(function () { $sendBtn.prop('disabled', false); });
435
+ });
436
+ $input.on('keydown', function (e) { if (e.key === 'Enter') $sendBtn.click(); });
437
+ $inputDiv.append($input, $sendBtn);
438
+ $modal.append($inputDiv);
439
+ $overlay.append($modal);
440
+ $overlay.on('click', function (e) { if (e.target === $overlay[0]) $overlay.remove(); });
441
+ $overlay.appendTo('body');
442
+ },
443
+ destroy: function () {
444
+ this.$fab.remove();
445
+ $('.fk-panel, .fk-detail-overlay, .fk-capture-overlay, .fk-capture-selection, .fk-capture-hint, #fk-styles').remove();
446
+ }
447
+ };
448
+ var _instance = null;
449
+ $.feedbackKit = function (options) {
450
+ if (typeof options === 'string') {
451
+ // Method calls: $.feedbackKit('setUser', {...})
452
+ if (options === 'setUser' && _instance) _instance.api.setUser(arguments[1]);
453
+ else if (options === 'clearUser' && _instance) _instance.api.clearUser();
454
+ else if (options === 'destroy' && _instance) { _instance.widget.destroy(); _instance = null; }
455
+ return;
456
+ }
457
+ if (!options || !options.apiKey) {
458
+ console.warn('[FeedbackKit] Missing apiKey');
459
+ return;
460
+ }
461
+ var api = new FeedbackKitAPI(options.apiKey);
462
+ var widget = new FeedbackKitWidget(api);
463
+ if (options.user) api.setUser(options.user);
464
+ _instance = { api: api, widget: widget };
465
+ return _instance;
466
+ };
467
+ })(jQuery);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@newsoftglobal/feedbackkit-jquery",
3
+ "version": "1.0.0",
4
+ "description": "FeedbackKit jQuery SDK — Collect user feedback with screenshots, comments, and tracking",
5
+ "author": "NewSoft Global",
6
+ "license": "MIT",
7
+ "main": "dist/feedbackkit-jquery.min.js",
8
+ "files": [
9
+ "dist/",
10
+ "README.md"
11
+ ],
12
+ "keywords": [
13
+ "feedback",
14
+ "widget",
15
+ "screenshot",
16
+ "bug-report",
17
+ "user-feedback",
18
+ "jquery",
19
+ "jquery-plugin"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "https://bitbucket.org/newsoftglobal/feedbackkit-jquery"
24
+ },
25
+ "scripts": {
26
+ "build": "node build.js",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "peerDependencies": {
30
+ "jquery": ">=3.0.0"
31
+ },
32
+ "devDependencies": {}
33
+ }