swarm-tickets 1.0.0 → 2.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.
@@ -0,0 +1,536 @@
1
+ /**
2
+ * Swarm Tickets Bug Report Widget
3
+ * Lightweight embeddable widget for end-user bug reporting
4
+ *
5
+ * Usage:
6
+ * <script src="https://your-server/bug-report-widget.js"
7
+ * data-endpoint="https://your-server/api/bug-report"
8
+ * data-api-key="stk_your_api_key"
9
+ * data-position="bottom-right"
10
+ * data-theme="dark">
11
+ * </script>
12
+ *
13
+ * Or programmatically:
14
+ * SwarmBugReport.init({
15
+ * endpoint: 'https://your-server/api/bug-report',
16
+ * apiKey: 'stk_your_api_key',
17
+ * position: 'bottom-right',
18
+ * theme: 'dark'
19
+ * });
20
+ */
21
+
22
+ (function() {
23
+ 'use strict';
24
+
25
+ // Prevent double initialization
26
+ if (window.SwarmBugReport) return;
27
+
28
+ const DEFAULT_CONFIG = {
29
+ endpoint: '/api/bug-report',
30
+ apiKey: null,
31
+ position: 'bottom-right', // bottom-right, bottom-left, top-right, top-left
32
+ theme: 'dark', // dark, light
33
+ buttonText: 'Report Bug',
34
+ buttonIcon: '🐛',
35
+ successMessage: 'Thank you! Your bug report has been submitted.',
36
+ errorMessage: 'Failed to submit bug report. Please try again.',
37
+ rateLimitMessage: 'Too many reports. Please wait a moment.',
38
+ collectErrors: true, // Automatically capture console errors
39
+ collectScreenshot: false, // Experimental: capture screenshot
40
+ maxErrors: 10 // Max errors to collect
41
+ };
42
+
43
+ let config = { ...DEFAULT_CONFIG };
44
+ let collectedErrors = [];
45
+ let isOpen = false;
46
+
47
+ // Styles
48
+ const STYLES = `
49
+ .swarm-bug-widget {
50
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
51
+ position: fixed;
52
+ z-index: 999999;
53
+ }
54
+
55
+ .swarm-bug-widget.bottom-right { bottom: 20px; right: 20px; }
56
+ .swarm-bug-widget.bottom-left { bottom: 20px; left: 20px; }
57
+ .swarm-bug-widget.top-right { top: 20px; right: 20px; }
58
+ .swarm-bug-widget.top-left { top: 20px; left: 20px; }
59
+
60
+ .swarm-bug-button {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 8px;
64
+ padding: 12px 20px;
65
+ border: none;
66
+ border-radius: 25px;
67
+ cursor: pointer;
68
+ font-size: 14px;
69
+ font-weight: 600;
70
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
71
+ transition: all 0.3s ease;
72
+ }
73
+
74
+ .swarm-bug-widget.dark .swarm-bug-button {
75
+ background: #2a2a2a;
76
+ color: #fff;
77
+ }
78
+
79
+ .swarm-bug-widget.light .swarm-bug-button {
80
+ background: #fff;
81
+ color: #333;
82
+ }
83
+
84
+ .swarm-bug-button:hover {
85
+ transform: translateY(-2px);
86
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
87
+ }
88
+
89
+ .swarm-bug-modal {
90
+ position: fixed;
91
+ inset: 0;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ z-index: 1000000;
96
+ opacity: 0;
97
+ visibility: hidden;
98
+ transition: all 0.3s ease;
99
+ }
100
+
101
+ .swarm-bug-modal.open {
102
+ opacity: 1;
103
+ visibility: visible;
104
+ }
105
+
106
+ .swarm-bug-overlay {
107
+ position: absolute;
108
+ inset: 0;
109
+ background: rgba(0, 0, 0, 0.5);
110
+ }
111
+
112
+ .swarm-bug-form-container {
113
+ position: relative;
114
+ width: 90%;
115
+ max-width: 500px;
116
+ max-height: 90vh;
117
+ overflow-y: auto;
118
+ border-radius: 12px;
119
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
120
+ transform: translateY(20px);
121
+ transition: transform 0.3s ease;
122
+ }
123
+
124
+ .swarm-bug-modal.open .swarm-bug-form-container {
125
+ transform: translateY(0);
126
+ }
127
+
128
+ .swarm-bug-widget.dark .swarm-bug-form-container {
129
+ background: #1a1a1a;
130
+ color: #e0e0e0;
131
+ }
132
+
133
+ .swarm-bug-widget.light .swarm-bug-form-container {
134
+ background: #fff;
135
+ color: #333;
136
+ }
137
+
138
+ .swarm-bug-header {
139
+ padding: 20px;
140
+ border-bottom: 1px solid rgba(128, 128, 128, 0.2);
141
+ display: flex;
142
+ justify-content: space-between;
143
+ align-items: center;
144
+ }
145
+
146
+ .swarm-bug-header h3 {
147
+ margin: 0;
148
+ font-size: 18px;
149
+ }
150
+
151
+ .swarm-bug-close {
152
+ background: none;
153
+ border: none;
154
+ font-size: 24px;
155
+ cursor: pointer;
156
+ padding: 0;
157
+ line-height: 1;
158
+ opacity: 0.7;
159
+ transition: opacity 0.2s;
160
+ }
161
+
162
+ .swarm-bug-widget.dark .swarm-bug-close { color: #fff; }
163
+ .swarm-bug-widget.light .swarm-bug-close { color: #333; }
164
+
165
+ .swarm-bug-close:hover { opacity: 1; }
166
+
167
+ .swarm-bug-form {
168
+ padding: 20px;
169
+ }
170
+
171
+ .swarm-bug-field {
172
+ margin-bottom: 16px;
173
+ }
174
+
175
+ .swarm-bug-field label {
176
+ display: block;
177
+ margin-bottom: 6px;
178
+ font-weight: 500;
179
+ font-size: 14px;
180
+ }
181
+
182
+ .swarm-bug-field input,
183
+ .swarm-bug-field textarea {
184
+ width: 100%;
185
+ padding: 10px 12px;
186
+ border-radius: 6px;
187
+ font-size: 14px;
188
+ font-family: inherit;
189
+ transition: border-color 0.2s;
190
+ }
191
+
192
+ .swarm-bug-widget.dark .swarm-bug-field input,
193
+ .swarm-bug-widget.dark .swarm-bug-field textarea {
194
+ background: #2a2a2a;
195
+ border: 1px solid #444;
196
+ color: #e0e0e0;
197
+ }
198
+
199
+ .swarm-bug-widget.light .swarm-bug-field input,
200
+ .swarm-bug-widget.light .swarm-bug-field textarea {
201
+ background: #f5f5f5;
202
+ border: 1px solid #ddd;
203
+ color: #333;
204
+ }
205
+
206
+ .swarm-bug-field input:focus,
207
+ .swarm-bug-field textarea:focus {
208
+ outline: none;
209
+ border-color: #00d4aa;
210
+ }
211
+
212
+ .swarm-bug-field textarea {
213
+ min-height: 100px;
214
+ resize: vertical;
215
+ }
216
+
217
+ .swarm-bug-errors-info {
218
+ padding: 10px 12px;
219
+ border-radius: 6px;
220
+ font-size: 12px;
221
+ margin-bottom: 16px;
222
+ }
223
+
224
+ .swarm-bug-widget.dark .swarm-bug-errors-info {
225
+ background: #3a2a2a;
226
+ border: 1px solid #5a3a3a;
227
+ color: #ff9999;
228
+ }
229
+
230
+ .swarm-bug-widget.light .swarm-bug-errors-info {
231
+ background: #fff5f5;
232
+ border: 1px solid #fcc;
233
+ color: #c00;
234
+ }
235
+
236
+ .swarm-bug-submit {
237
+ width: 100%;
238
+ padding: 12px;
239
+ border: none;
240
+ border-radius: 6px;
241
+ background: #00d4aa;
242
+ color: #1a1a1a;
243
+ font-size: 14px;
244
+ font-weight: 600;
245
+ cursor: pointer;
246
+ transition: all 0.2s;
247
+ }
248
+
249
+ .swarm-bug-submit:hover {
250
+ background: #00ffcc;
251
+ }
252
+
253
+ .swarm-bug-submit:disabled {
254
+ opacity: 0.5;
255
+ cursor: not-allowed;
256
+ }
257
+
258
+ .swarm-bug-status {
259
+ padding: 12px;
260
+ border-radius: 6px;
261
+ margin-top: 16px;
262
+ text-align: center;
263
+ font-size: 14px;
264
+ }
265
+
266
+ .swarm-bug-status.success {
267
+ background: #2d4a2d;
268
+ color: #6bcf7f;
269
+ }
270
+
271
+ .swarm-bug-status.error {
272
+ background: #4a2d2d;
273
+ color: #ff6b6b;
274
+ }
275
+
276
+ .swarm-bug-footer {
277
+ padding: 12px 20px;
278
+ border-top: 1px solid rgba(128, 128, 128, 0.2);
279
+ text-align: center;
280
+ font-size: 11px;
281
+ opacity: 0.6;
282
+ }
283
+ `;
284
+
285
+ // Inject styles
286
+ function injectStyles() {
287
+ const style = document.createElement('style');
288
+ style.textContent = STYLES;
289
+ document.head.appendChild(style);
290
+ }
291
+
292
+ // Create widget HTML
293
+ function createWidget() {
294
+ const widget = document.createElement('div');
295
+ widget.className = `swarm-bug-widget ${config.position} ${config.theme}`;
296
+ widget.innerHTML = `
297
+ <button class="swarm-bug-button" aria-label="Report a bug">
298
+ <span>${config.buttonIcon}</span>
299
+ <span>${config.buttonText}</span>
300
+ </button>
301
+
302
+ <div class="swarm-bug-modal">
303
+ <div class="swarm-bug-overlay"></div>
304
+ <div class="swarm-bug-form-container">
305
+ <div class="swarm-bug-header">
306
+ <h3>${config.buttonIcon} Report a Bug</h3>
307
+ <button class="swarm-bug-close" aria-label="Close">&times;</button>
308
+ </div>
309
+
310
+ <form class="swarm-bug-form">
311
+ <div class="swarm-bug-field">
312
+ <label for="swarm-bug-description">What happened?</label>
313
+ <textarea id="swarm-bug-description" placeholder="Please describe what went wrong..." required></textarea>
314
+ </div>
315
+
316
+ <div class="swarm-bug-field">
317
+ <label for="swarm-bug-steps">Steps to reproduce (optional)</label>
318
+ <textarea id="swarm-bug-steps" placeholder="1. Go to...\n2. Click on...\n3. See error"></textarea>
319
+ </div>
320
+
321
+ <div class="swarm-bug-errors-info" style="display: none;"></div>
322
+
323
+ <button type="submit" class="swarm-bug-submit">Submit Report</button>
324
+
325
+ <div class="swarm-bug-status" style="display: none;"></div>
326
+ </form>
327
+
328
+ <div class="swarm-bug-footer">
329
+ Powered by Swarm Tickets
330
+ </div>
331
+ </div>
332
+ </div>
333
+ `;
334
+
335
+ document.body.appendChild(widget);
336
+ return widget;
337
+ }
338
+
339
+ // Set up error collection
340
+ function setupErrorCollection() {
341
+ if (!config.collectErrors) return;
342
+
343
+ const originalConsoleError = console.error;
344
+ console.error = function(...args) {
345
+ if (collectedErrors.length < config.maxErrors) {
346
+ collectedErrors.push({
347
+ type: 'console.error',
348
+ message: args.map(a => String(a)).join(' '),
349
+ timestamp: new Date().toISOString()
350
+ });
351
+ }
352
+ originalConsoleError.apply(console, args);
353
+ };
354
+
355
+ window.addEventListener('error', function(event) {
356
+ if (collectedErrors.length < config.maxErrors) {
357
+ collectedErrors.push({
358
+ type: 'window.error',
359
+ message: event.message,
360
+ filename: event.filename,
361
+ lineno: event.lineno,
362
+ colno: event.colno,
363
+ timestamp: new Date().toISOString()
364
+ });
365
+ }
366
+ });
367
+
368
+ window.addEventListener('unhandledrejection', function(event) {
369
+ if (collectedErrors.length < config.maxErrors) {
370
+ collectedErrors.push({
371
+ type: 'unhandledrejection',
372
+ message: String(event.reason),
373
+ timestamp: new Date().toISOString()
374
+ });
375
+ }
376
+ });
377
+ }
378
+
379
+ // Initialize widget
380
+ function init(userConfig = {}) {
381
+ config = { ...DEFAULT_CONFIG, ...userConfig };
382
+
383
+ injectStyles();
384
+ const widget = createWidget();
385
+ setupErrorCollection();
386
+
387
+ const button = widget.querySelector('.swarm-bug-button');
388
+ const modal = widget.querySelector('.swarm-bug-modal');
389
+ const overlay = widget.querySelector('.swarm-bug-overlay');
390
+ const closeBtn = widget.querySelector('.swarm-bug-close');
391
+ const form = widget.querySelector('.swarm-bug-form');
392
+ const errorsInfo = widget.querySelector('.swarm-bug-errors-info');
393
+ const status = widget.querySelector('.swarm-bug-status');
394
+
395
+ function openModal() {
396
+ isOpen = true;
397
+ modal.classList.add('open');
398
+
399
+ // Show error count
400
+ if (collectedErrors.length > 0) {
401
+ errorsInfo.style.display = 'block';
402
+ errorsInfo.textContent = `📋 ${collectedErrors.length} error(s) captured from this session will be included.`;
403
+ } else {
404
+ errorsInfo.style.display = 'none';
405
+ }
406
+
407
+ status.style.display = 'none';
408
+ }
409
+
410
+ function closeModal() {
411
+ isOpen = false;
412
+ modal.classList.remove('open');
413
+ }
414
+
415
+ button.addEventListener('click', openModal);
416
+ overlay.addEventListener('click', closeModal);
417
+ closeBtn.addEventListener('click', closeModal);
418
+
419
+ document.addEventListener('keydown', (e) => {
420
+ if (e.key === 'Escape' && isOpen) closeModal();
421
+ });
422
+
423
+ form.addEventListener('submit', async (e) => {
424
+ e.preventDefault();
425
+
426
+ const submitBtn = form.querySelector('.swarm-bug-submit');
427
+ submitBtn.disabled = true;
428
+ submitBtn.textContent = 'Submitting...';
429
+ status.style.display = 'none';
430
+
431
+ const description = form.querySelector('#swarm-bug-description').value;
432
+ const steps = form.querySelector('#swarm-bug-steps').value;
433
+
434
+ const report = {
435
+ description: description + (steps ? '\n\nSteps to reproduce:\n' + steps : ''),
436
+ location: window.location.href,
437
+ clientError: collectedErrors.length > 0
438
+ ? collectedErrors.map(e => `[${e.type}] ${e.message}`).join('\n')
439
+ : '',
440
+ userAgent: navigator.userAgent,
441
+ timestamp: new Date().toISOString(),
442
+ viewport: `${window.innerWidth}x${window.innerHeight}`
443
+ };
444
+
445
+ try {
446
+ const headers = {
447
+ 'Content-Type': 'application/json'
448
+ };
449
+
450
+ if (config.apiKey) {
451
+ headers['X-API-Key'] = config.apiKey;
452
+ }
453
+
454
+ const response = await fetch(config.endpoint, {
455
+ method: 'POST',
456
+ headers,
457
+ body: JSON.stringify(report)
458
+ });
459
+
460
+ const data = await response.json();
461
+
462
+ if (response.ok) {
463
+ status.className = 'swarm-bug-status success';
464
+ status.textContent = config.successMessage;
465
+ status.style.display = 'block';
466
+ form.reset();
467
+ collectedErrors = []; // Clear errors after successful submit
468
+
469
+ setTimeout(() => closeModal(), 2000);
470
+ } else if (response.status === 429) {
471
+ status.className = 'swarm-bug-status error';
472
+ status.textContent = config.rateLimitMessage;
473
+ status.style.display = 'block';
474
+ } else {
475
+ throw new Error(data.error || 'Unknown error');
476
+ }
477
+ } catch (error) {
478
+ status.className = 'swarm-bug-status error';
479
+ status.textContent = config.errorMessage;
480
+ status.style.display = 'block';
481
+ console.error('Bug report submission failed:', error);
482
+ } finally {
483
+ submitBtn.disabled = false;
484
+ submitBtn.textContent = 'Submit Report';
485
+ }
486
+ });
487
+ }
488
+
489
+ // Auto-initialize from script tag attributes
490
+ function autoInit() {
491
+ const script = document.currentScript || document.querySelector('script[data-endpoint]');
492
+ if (!script) return;
493
+
494
+ const attrs = {
495
+ endpoint: script.getAttribute('data-endpoint'),
496
+ apiKey: script.getAttribute('data-api-key'),
497
+ position: script.getAttribute('data-position'),
498
+ theme: script.getAttribute('data-theme'),
499
+ buttonText: script.getAttribute('data-button-text'),
500
+ buttonIcon: script.getAttribute('data-button-icon')
501
+ };
502
+
503
+ // Remove null/undefined values
504
+ Object.keys(attrs).forEach(key => {
505
+ if (attrs[key] === null || attrs[key] === undefined) {
506
+ delete attrs[key];
507
+ }
508
+ });
509
+
510
+ if (Object.keys(attrs).length > 0) {
511
+ if (document.readyState === 'loading') {
512
+ document.addEventListener('DOMContentLoaded', () => init(attrs));
513
+ } else {
514
+ init(attrs);
515
+ }
516
+ }
517
+ }
518
+
519
+ // Export API
520
+ window.SwarmBugReport = {
521
+ init,
522
+ open: () => {
523
+ const modal = document.querySelector('.swarm-bug-modal');
524
+ if (modal) modal.classList.add('open');
525
+ },
526
+ close: () => {
527
+ const modal = document.querySelector('.swarm-bug-modal');
528
+ if (modal) modal.classList.remove('open');
529
+ },
530
+ getErrors: () => [...collectedErrors],
531
+ clearErrors: () => { collectedErrors = []; }
532
+ };
533
+
534
+ // Auto-initialize
535
+ autoInit();
536
+ })();