@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 +78 -0
- package/dist/feedbackkit-jquery.js +555 -0
- package/dist/feedbackkit-jquery.min.js +467 -0
- package/package.json +33 -0
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
|
+
}
|