@uxbertlabs/reportly 1.0.17 → 1.0.19

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.
@@ -182,11 +182,11 @@ class Modal {
182
182
  }
183
183
  create() {
184
184
  // Create overlay
185
- this.overlay = document.createElement('div');
186
- this.overlay.className = 'uxbert-overlay';
185
+ this.overlay = document.createElement("div");
186
+ this.overlay.className = "uxbert-overlay";
187
187
  // Create modal
188
- this.modal = document.createElement('div');
189
- this.modal.className = 'uxbert-modal';
188
+ this.modal = document.createElement("div");
189
+ this.modal.className = "uxbert-modal";
190
190
  // Modal HTML
191
191
  this.modal.innerHTML = `
192
192
  <div class="uxbert-modal-header">
@@ -231,12 +231,13 @@ class Modal {
231
231
 
232
232
  <div class="uxbert-form-group">
233
233
  <label class="uxbert-form-label" for="uxbert-priority">Priority</label>
234
- <select id="uxbert-priority" class="uxbert-form-select">
235
- <option value="Low">Low</option>
236
- <option value="Medium" selected>Medium</option>
237
- <option value="High">High</option>
238
- <option value="Critical">Critical</option>
239
- </select>
234
+ <select id="uxbert-priority" class="uxbert-form-select">
235
+ <option value="5" selected>Lowest</option>
236
+ <option value="4">Low</option>
237
+ <option value="3" selected>Medium</option>
238
+ <option value="2">High</option>
239
+ <option value="1">Highest</option>
240
+ </select>
240
241
  </div>
241
242
 
242
243
  <div class="uxbert-capture-action" id="uxbert-capture-action">
@@ -261,6 +262,9 @@ class Modal {
261
262
  <button type="button" class="uxbert-btn uxbert-btn-secondary" id="uxbert-cancel-btn">
262
263
  Cancel
263
264
  </button>
265
+ <button type="button" class="uxbert-btn uxbert-btn-success" id="uxbert-n8n-btn" style="display: none;">
266
+ 🚀 Send to n8n
267
+ </button>
264
268
  <button type="submit" class="uxbert-btn uxbert-btn-primary" id="uxbert-submit-btn">
265
269
  📥 Download JSON
266
270
  </button>
@@ -277,72 +281,103 @@ class Modal {
277
281
  if (!this.modal)
278
282
  return;
279
283
  // Close button
280
- const closeBtn = this.modal.querySelector('.uxbert-modal-close');
281
- closeBtn.addEventListener('click', () => this.close());
284
+ const closeBtn = this.modal.querySelector(".uxbert-modal-close");
285
+ closeBtn.addEventListener("click", () => this.close());
282
286
  // Cancel button
283
- const cancelBtn = this.modal.querySelector('#uxbert-cancel-btn');
284
- cancelBtn.addEventListener('click', () => this.close());
287
+ const cancelBtn = this.modal.querySelector("#uxbert-cancel-btn");
288
+ cancelBtn.addEventListener("click", () => this.close());
285
289
  // Overlay click to close
286
- this.overlay?.addEventListener('click', (e) => {
290
+ this.overlay?.addEventListener("click", (e) => {
287
291
  if (e.target === this.overlay) {
288
292
  this.close();
289
293
  }
290
294
  });
291
295
  // Form submit
292
- const form = this.modal.querySelector('#uxbert-issue-form');
293
- form.addEventListener('submit', (e) => {
296
+ const form = this.modal.querySelector("#uxbert-issue-form");
297
+ form.addEventListener("submit", (e) => {
294
298
  e.preventDefault();
295
299
  this.handleSubmit();
296
300
  });
297
301
  // Capture button
298
- const captureBtn = this.modal.querySelector('#uxbert-capture-btn');
299
- captureBtn.addEventListener('click', () => {
302
+ const captureBtn = this.modal.querySelector("#uxbert-capture-btn");
303
+ captureBtn.addEventListener("click", () => {
300
304
  if (this.callbacks.onCapture) {
301
305
  this.callbacks.onCapture();
302
306
  }
303
307
  });
304
308
  // Annotate button
305
- const annotateBtn = this.modal.querySelector('#uxbert-annotate-btn');
306
- annotateBtn.addEventListener('click', () => {
309
+ const annotateBtn = this.modal.querySelector("#uxbert-annotate-btn");
310
+ annotateBtn.addEventListener("click", () => {
307
311
  if (this.callbacks.onAnnotate) {
308
312
  this.callbacks.onAnnotate();
309
313
  }
310
314
  });
311
315
  // Retake button
312
- const retakeBtn = this.modal.querySelector('#uxbert-retake-btn');
313
- retakeBtn.addEventListener('click', () => {
316
+ const retakeBtn = this.modal.querySelector("#uxbert-retake-btn");
317
+ retakeBtn.addEventListener("click", () => {
314
318
  if (this.callbacks.onRetake) {
315
319
  this.callbacks.onRetake();
316
320
  }
317
321
  });
322
+ // n8n button
323
+ const n8nBtn = this.modal.querySelector("#uxbert-n8n-btn");
324
+ n8nBtn.addEventListener("click", () => {
325
+ this.handleN8nSubmit();
326
+ });
327
+ }
328
+ handleN8nSubmit() {
329
+ if (!this.modal)
330
+ return;
331
+ const titleInput = this.modal.querySelector("#uxbert-title");
332
+ const descriptionInput = this.modal.querySelector("#uxbert-description");
333
+ const prioritySelect = this.modal.querySelector("#uxbert-priority");
334
+ const title = titleInput.value;
335
+ const description = descriptionInput.value;
336
+ const priority = prioritySelect.value;
337
+ if (!title.trim()) {
338
+ alert("Please enter an issue title");
339
+ return;
340
+ }
341
+ const issueData = {
342
+ title: title.trim(),
343
+ description: description.trim(),
344
+ priority,
345
+ };
346
+ if (this.callbacks.onSendToN8n) {
347
+ this.callbacks.onSendToN8n(issueData);
348
+ }
318
349
  }
319
350
  handleSubmit() {
320
351
  if (!this.modal)
321
352
  return;
322
- const titleInput = this.modal.querySelector('#uxbert-title');
323
- const descriptionInput = this.modal.querySelector('#uxbert-description');
324
- const prioritySelect = this.modal.querySelector('#uxbert-priority');
353
+ const titleInput = this.modal.querySelector("#uxbert-title");
354
+ const descriptionInput = this.modal.querySelector("#uxbert-description");
355
+ const prioritySelect = this.modal.querySelector("#uxbert-priority");
325
356
  const title = titleInput.value;
326
357
  const description = descriptionInput.value;
327
358
  const priority = prioritySelect.value;
328
359
  if (!title.trim()) {
329
- alert('Please enter an issue title');
360
+ alert("Please enter an issue title");
330
361
  return;
331
362
  }
332
363
  const issueData = {
333
364
  title: title.trim(),
334
365
  description: description.trim(),
335
- priority
366
+ priority,
336
367
  };
337
368
  if (this.callbacks.onSubmit) {
338
369
  this.callbacks.onSubmit(issueData);
339
370
  }
340
371
  }
341
372
  open() {
342
- this.overlay?.classList.add('active');
373
+ this.overlay?.classList.add("active");
374
+ // Prevent body scroll when modal is open
375
+ document.body.style.overflow = "hidden";
343
376
  }
344
377
  close() {
345
- this.overlay?.classList.remove('active');
378
+ this.overlay?.classList.remove("active");
379
+ // Restore body scroll when modal is closed
380
+ document.body.style.overflow = "";
346
381
  this.reset();
347
382
  if (this.callbacks.onClose) {
348
383
  this.callbacks.onClose();
@@ -351,39 +386,49 @@ class Modal {
351
386
  setScreenshot(screenshot) {
352
387
  if (!this.modal)
353
388
  return;
354
- const container = this.modal.querySelector('#uxbert-screenshot-container');
355
- const img = this.modal.querySelector('#uxbert-screenshot-img');
356
- const captureAction = this.modal.querySelector('#uxbert-capture-action');
357
- const submitBtn = this.modal.querySelector('#uxbert-submit-btn');
389
+ const container = this.modal.querySelector("#uxbert-screenshot-container");
390
+ const img = this.modal.querySelector("#uxbert-screenshot-img");
391
+ const captureAction = this.modal.querySelector("#uxbert-capture-action");
392
+ const submitBtn = this.modal.querySelector("#uxbert-submit-btn");
358
393
  if (screenshot) {
359
394
  img.src = screenshot;
360
- container.style.display = 'block';
361
- captureAction.style.display = 'none';
395
+ container.style.display = "block";
396
+ captureAction.style.display = "none";
362
397
  submitBtn.disabled = false;
363
398
  }
364
399
  else {
365
- container.style.display = 'none';
366
- captureAction.style.display = 'block';
400
+ container.style.display = "none";
401
+ captureAction.style.display = "block";
367
402
  submitBtn.disabled = true;
368
403
  }
369
404
  }
370
405
  getCaptureMode() {
371
406
  if (!this.modal)
372
- return 'viewport';
373
- const viewportRadio = this.modal.querySelector('#uxbert-capture-viewport');
374
- return viewportRadio.checked ? 'viewport' : 'fullpage';
407
+ return "viewport";
408
+ const viewportRadio = this.modal.querySelector("#uxbert-capture-viewport");
409
+ return viewportRadio.checked ? "viewport" : "fullpage";
410
+ }
411
+ setN8nButtonVisible(visible) {
412
+ if (!this.modal)
413
+ return;
414
+ const n8nBtn = this.modal.querySelector("#uxbert-n8n-btn");
415
+ if (n8nBtn) {
416
+ n8nBtn.style.display = visible ? "inline-block" : "none";
417
+ }
375
418
  }
376
419
  reset() {
377
420
  if (!this.modal)
378
421
  return;
379
- this.modal.querySelector('#uxbert-title').value = '';
380
- this.modal.querySelector('#uxbert-description').value = '';
381
- this.modal.querySelector('#uxbert-priority').value = 'Medium';
422
+ this.modal.querySelector("#uxbert-title").value = "";
423
+ this.modal.querySelector("#uxbert-description").value = "";
424
+ this.modal.querySelector("#uxbert-priority").value = "Medium";
382
425
  this.setScreenshot(null);
383
426
  }
384
427
  destroy() {
385
428
  if (this.overlay && this.overlay.parentNode) {
386
429
  this.overlay.parentNode.removeChild(this.overlay);
430
+ // Restore body scroll when destroying modal
431
+ document.body.style.overflow = "";
387
432
  this.overlay = null;
388
433
  this.modal = null;
389
434
  }
@@ -10159,9 +10204,223 @@ class DeviceInfo {
10159
10204
  }
10160
10205
  }
10161
10206
 
10207
+ class N8nIntegration {
10208
+ constructor(config) {
10209
+ this.config = config || null;
10210
+ }
10211
+ /**
10212
+ * Configure n8n integration
10213
+ */
10214
+ configure(config) {
10215
+ this.config = config;
10216
+ }
10217
+ /**
10218
+ * Check if n8n integration is configured and enabled
10219
+ */
10220
+ isEnabled() {
10221
+ return !!(this.config && this.config.enabled && this.config.webhookUrl);
10222
+ }
10223
+ /**
10224
+ * Send issue data to n8n webhook
10225
+ */
10226
+ async sendToN8n(issueData) {
10227
+ if (!this.config || !this.config.webhookUrl) {
10228
+ return {
10229
+ success: false,
10230
+ message: "n8n webhook URL is not configured",
10231
+ error: "MISSING_CONFIG",
10232
+ };
10233
+ }
10234
+ if (!this.config.enabled) {
10235
+ return {
10236
+ success: false,
10237
+ message: "n8n integration is disabled",
10238
+ error: "DISABLED",
10239
+ };
10240
+ }
10241
+ try {
10242
+ // Prepare FormData with binary screenshot
10243
+ const formData = await this.prepareFormData(issueData);
10244
+ // Setup request headers (don't set Content-Type - browser will set it with boundary for FormData)
10245
+ const headers = {
10246
+ ...this.config.headers,
10247
+ };
10248
+ // Setup timeout
10249
+ const timeout = this.config.timeout || 30000; // 30 seconds default
10250
+ const controller = new AbortController();
10251
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
10252
+ // Send request
10253
+ const response = await fetch(this.config.webhookUrl, {
10254
+ method: "POST",
10255
+ headers,
10256
+ body: formData,
10257
+ signal: controller.signal,
10258
+ });
10259
+ clearTimeout(timeoutId);
10260
+ // Check response
10261
+ if (!response.ok) {
10262
+ const errorText = await response.text().catch(() => "Unknown error");
10263
+ return {
10264
+ success: false,
10265
+ message: `n8n webhook request failed: ${response.status} ${response.statusText}`,
10266
+ error: errorText,
10267
+ };
10268
+ }
10269
+ // Parse response
10270
+ let responseData;
10271
+ try {
10272
+ responseData = await response.json();
10273
+ }
10274
+ catch (e) {
10275
+ // Some webhooks don't return JSON
10276
+ responseData = { message: "Success" };
10277
+ }
10278
+ return {
10279
+ success: true,
10280
+ message: "Issue sent to n8n successfully",
10281
+ workflowId: responseData.workflowId || responseData.executionId,
10282
+ };
10283
+ }
10284
+ catch (error) {
10285
+ if (error instanceof Error) {
10286
+ if (error.name === "AbortError") {
10287
+ return {
10288
+ success: false,
10289
+ message: "Request to n8n webhook timed out",
10290
+ error: "TIMEOUT",
10291
+ };
10292
+ }
10293
+ return {
10294
+ success: false,
10295
+ message: `Failed to send issue to n8n: ${error.message}`,
10296
+ error: error.message,
10297
+ };
10298
+ }
10299
+ return {
10300
+ success: false,
10301
+ message: "Unknown error occurred while sending to n8n",
10302
+ error: "UNKNOWN",
10303
+ };
10304
+ }
10305
+ }
10306
+ /**
10307
+ * Convert base64 data URL to Blob
10308
+ */
10309
+ base64ToBlob(base64Data) {
10310
+ // Extract the base64 string and mime type
10311
+ const matches = base64Data.match(/^data:([^;]+);base64,(.+)$/);
10312
+ if (!matches) {
10313
+ throw new Error("Invalid base64 data URL");
10314
+ }
10315
+ const mimeType = matches[1];
10316
+ const base64String = matches[2];
10317
+ // Decode base64
10318
+ const byteCharacters = atob(base64String);
10319
+ const byteNumbers = new Array(byteCharacters.length);
10320
+ for (let i = 0; i < byteCharacters.length; i++) {
10321
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
10322
+ }
10323
+ const byteArray = new Uint8Array(byteNumbers);
10324
+ return new Blob([byteArray], { type: mimeType });
10325
+ }
10326
+ /**
10327
+ * Prepare FormData payload for n8n webhook
10328
+ * This sends the screenshot as a binary file instead of base64 JSON
10329
+ */
10330
+ async prepareFormData(issueData) {
10331
+ const formData = new FormData();
10332
+ // Add issue data as JSON string
10333
+ formData.append("title", issueData.title);
10334
+ formData.append("description", issueData.description);
10335
+ formData.append("priority", issueData.priority);
10336
+ formData.append("createdAt", issueData.createdAt);
10337
+ // Convert base64 screenshot to binary Blob and add as file
10338
+ const screenshotBlob = this.base64ToBlob(issueData.screenshot);
10339
+ const filename = `screenshot-${Date.now()}.png`;
10340
+ formData.append("screenshot", screenshotBlob, filename);
10341
+ // Add device info as individual fields
10342
+ formData.append("browser", issueData.deviceInfo.browser);
10343
+ formData.append("os", issueData.deviceInfo.os);
10344
+ formData.append("screenResolution", issueData.deviceInfo.screenResolution);
10345
+ formData.append("viewport", issueData.deviceInfo.viewport);
10346
+ formData.append("url", issueData.deviceInfo.url);
10347
+ formData.append("userAgent", issueData.deviceInfo.userAgent);
10348
+ formData.append("language", issueData.deviceInfo.language || "");
10349
+ formData.append("platform", issueData.deviceInfo.platform);
10350
+ formData.append("cookiesEnabled", String(issueData.deviceInfo.cookiesEnabled));
10351
+ formData.append("onLine", String(issueData.deviceInfo.onLine));
10352
+ formData.append("timestamp", issueData.deviceInfo.timestamp);
10353
+ // Add metadata
10354
+ formData.append("source", "uxbert-reportly");
10355
+ formData.append("version", "1.0.0");
10356
+ return formData;
10357
+ }
10358
+ /**
10359
+ * Test connection to n8n webhook
10360
+ */
10361
+ async testConnection() {
10362
+ if (!this.config || !this.config.webhookUrl) {
10363
+ return {
10364
+ success: false,
10365
+ message: "n8n webhook URL is not configured",
10366
+ error: "MISSING_CONFIG",
10367
+ };
10368
+ }
10369
+ try {
10370
+ const controller = new AbortController();
10371
+ const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout for test
10372
+ await fetch(this.config.webhookUrl, {
10373
+ method: "HEAD",
10374
+ signal: controller.signal,
10375
+ });
10376
+ clearTimeout(timeoutId);
10377
+ return {
10378
+ success: true,
10379
+ message: "n8n webhook is reachable",
10380
+ };
10381
+ }
10382
+ catch (error) {
10383
+ return {
10384
+ success: false,
10385
+ message: "Failed to reach n8n webhook",
10386
+ error: error instanceof Error ? error.message : "UNKNOWN",
10387
+ };
10388
+ }
10389
+ }
10390
+ /**
10391
+ * Get current configuration
10392
+ */
10393
+ getConfig() {
10394
+ return this.config;
10395
+ }
10396
+ /**
10397
+ * Update webhook URL
10398
+ */
10399
+ setWebhookUrl(url) {
10400
+ if (!this.config) {
10401
+ this.config = {
10402
+ webhookUrl: url,
10403
+ enabled: true,
10404
+ };
10405
+ }
10406
+ else {
10407
+ this.config.webhookUrl = url;
10408
+ }
10409
+ }
10410
+ /**
10411
+ * Enable/disable n8n integration
10412
+ */
10413
+ setEnabled(enabled) {
10414
+ if (this.config) {
10415
+ this.config.enabled = enabled;
10416
+ }
10417
+ }
10418
+ }
10419
+
10162
10420
  class Export {
10163
- constructor() {
10421
+ constructor(n8nConfig) {
10164
10422
  this.savedIssues = this.loadSavedIssues();
10423
+ this.n8nIntegration = new N8nIntegration(n8nConfig);
10165
10424
  }
10166
10425
  exportToJSON(issueData) {
10167
10426
  const json = JSON.stringify(issueData, null, 2);
@@ -10230,6 +10489,54 @@ class Export {
10230
10489
  document.body.removeChild(link);
10231
10490
  URL.revokeObjectURL(url);
10232
10491
  }
10492
+ // n8n Integration Methods
10493
+ /**
10494
+ * Send issue to n8n webhook
10495
+ */
10496
+ async sendToN8n(issueData) {
10497
+ const response = await this.n8nIntegration.sendToN8n(issueData);
10498
+ // Save to localStorage if n8n send was successful
10499
+ if (response.success) {
10500
+ this.saveIssue(issueData);
10501
+ }
10502
+ return response;
10503
+ }
10504
+ /**
10505
+ * Configure n8n integration
10506
+ */
10507
+ configureN8n(config) {
10508
+ this.n8nIntegration.configure(config);
10509
+ }
10510
+ /**
10511
+ * Check if n8n is enabled
10512
+ */
10513
+ isN8nEnabled() {
10514
+ return this.n8nIntegration.isEnabled();
10515
+ }
10516
+ /**
10517
+ * Test n8n connection
10518
+ */
10519
+ async testN8nConnection() {
10520
+ return this.n8nIntegration.testConnection();
10521
+ }
10522
+ /**
10523
+ * Get n8n configuration
10524
+ */
10525
+ getN8nConfig() {
10526
+ return this.n8nIntegration.getConfig();
10527
+ }
10528
+ /**
10529
+ * Enable/disable n8n integration
10530
+ */
10531
+ setN8nEnabled(enabled) {
10532
+ this.n8nIntegration.setEnabled(enabled);
10533
+ }
10534
+ /**
10535
+ * Set n8n webhook URL
10536
+ */
10537
+ setN8nWebhookUrl(url) {
10538
+ this.n8nIntegration.setWebhookUrl(url);
10539
+ }
10233
10540
  }
10234
10541
 
10235
10542
  function styleInject(css, ref) {
@@ -10259,7 +10566,7 @@ function styleInject(css, ref) {
10259
10566
  }
10260
10567
  }
10261
10568
 
10262
- var css_248z = ":root{--uxbert-primary:#4f46e5;--uxbert-primary-hover:#4338ca;--uxbert-danger:#ef4444;--uxbert-success:#10b981;--uxbert-bg:#fff;--uxbert-text:#1f2937;--uxbert-border:#e5e7eb;--uxbert-shadow:0 10px 25px rgba(0,0,0,.1);--uxbert-z-button:999999;--uxbert-z-modal:1000000;--uxbert-z-canvas:1000001;--uxbert-z-toolbar:1000002}[data-theme=dark]{--uxbert-bg:#1f2937;--uxbert-text:#f9fafb;--uxbert-border:#374151}.uxbert-fab{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:flex;font-size:24px;height:56px;justify-content:center;position:fixed;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-button)}.uxbert-fab:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-fab.bottom-right{bottom:24px;right:24px}.uxbert-fab.bottom-left{bottom:24px;left:24px}.uxbert-fab.top-right{right:24px;top:24px}.uxbert-fab.top-left{left:24px;top:24px}.uxbert-overlay{align-items:center;animation:fadeIn .3s ease;background:rgba(0,0,0,.5);display:none;height:100%;justify-content:center;left:0;position:fixed;top:0;width:100%;z-index:var(--uxbert-z-modal)}.uxbert-overlay.active{display:flex}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.uxbert-modal{animation:slideUp .3s ease;background:var(--uxbert-bg);border-radius:12px;box-shadow:var(--uxbert-shadow);color:var(--uxbert-text);max-height:90vh;max-width:600px;overflow-y:auto;padding:24px;width:90%}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.uxbert-modal-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.uxbert-modal-title{font-size:20px;font-weight:600;margin:0}.uxbert-modal-close{align-items:center;background:none;border:none;border-radius:4px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:24px;height:32px;justify-content:center;padding:0;width:32px}.uxbert-modal-close:hover{background:var(--uxbert-border)}.uxbert-form-group{margin-bottom:16px}.uxbert-form-label{display:block;font-size:14px;font-weight:500;margin-bottom:8px}.uxbert-capture-mode{display:flex;flex-wrap:wrap;gap:12px}.uxbert-radio-label{align-items:center;border:2px solid var(--uxbert-border);border-radius:8px;cursor:pointer;display:flex;flex:1;gap:8px;min-width:140px;padding:10px 16px;transition:all .2s ease}.uxbert-radio-label:hover{background:rgba(79,70,229,.05);border-color:var(--uxbert-primary)}.uxbert-radio-label input[type=radio]{cursor:pointer;height:18px;margin:0;width:18px}.uxbert-radio-label input[type=radio]:checked{accent-color:var(--uxbert-primary)}.uxbert-radio-label:has(input[type=radio]:checked){background:rgba(79,70,229,.1);border-color:var(--uxbert-primary)}.uxbert-radio-label span{flex:1;font-size:14px;font-weight:500}.uxbert-form-input,.uxbert-form-select,.uxbert-form-textarea{background:var(--uxbert-bg);border:1px solid var(--uxbert-border);border-radius:6px;box-sizing:border-box;color:var(--uxbert-text);font-family:inherit;font-size:14px;padding:10px;width:100%}.uxbert-form-textarea{min-height:100px;resize:vertical}.uxbert-form-input:focus,.uxbert-form-select:focus,.uxbert-form-textarea:focus{border-color:var(--uxbert-primary);outline:none}.uxbert-capture-action{margin:16px 0;text-align:center}.uxbert-capture-action .uxbert-btn{min-width:200px}.uxbert-screenshot-preview{border:1px solid var(--uxbert-border);border-radius:8px;margin:16px 0;overflow:hidden}.uxbert-screenshot-preview img{display:block;width:100%}.uxbert-screenshot-actions{background:var(--uxbert-border);display:flex;gap:8px;padding:12px}.uxbert-btn{align-items:center;border:none;border-radius:6px;cursor:pointer;display:inline-flex;font-size:14px;font-weight:500;gap:8px;padding:10px 20px;transition:all .2s ease}.uxbert-btn-primary{background:var(--uxbert-primary);color:#fff}.uxbert-btn-primary:hover{background:var(--uxbert-primary-hover)}.uxbert-btn-secondary{background:var(--uxbert-border);color:var(--uxbert-text)}.uxbert-btn-secondary:hover{background:#d1d5db}.uxbert-btn-danger{background:var(--uxbert-danger);color:#fff}.uxbert-btn-danger:hover{background:#dc2626}.uxbert-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}.uxbert-toolbar-toggle{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;bottom:24px;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:none;font-size:24px;height:56px;justify-content:center;position:fixed;right:24px;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar-toggle.active{display:flex}.uxbert-toolbar-toggle.hidden{opacity:0;pointer-events:none;transform:scale(.8)}.uxbert-toolbar-toggle:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-toolbar{background:var(--uxbert-bg);border-radius:12px;bottom:24px;box-shadow:var(--uxbert-shadow);display:none;flex-direction:column;gap:8px;max-height:80vh;opacity:0;overflow-y:auto;padding:16px;pointer-events:none;position:fixed;right:24px;transform:translateY(20px);transition:all .3s ease;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar.active{display:flex}.uxbert-toolbar.expanded{opacity:1;pointer-events:all;transform:translateY(0)}.uxbert-toolbar-header{align-items:center;display:flex;gap:8px;justify-content:space-between;margin-bottom:8px}.uxbert-toolbar-title{flex:1;font-size:14px;font-weight:600}.uxbert-toolbar-exit,.uxbert-toolbar-minimize{align-items:center;background:var(--uxbert-border);border:none;border-radius:6px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:20px;font-weight:700;height:32px;justify-content:center;transition:all .2s ease;width:32px}.uxbert-toolbar-exit{background:var(--uxbert-danger);color:#fff}.uxbert-toolbar-minimize:hover{background:#d1d5db;transform:scale(1.1)}.uxbert-toolbar-exit:hover{background:#dc2626;transform:scale(1.1)}.uxbert-toolbar-tools{display:flex;flex-wrap:wrap;gap:8px}.uxbert-tool-btn{align-items:center;background:var(--uxbert-bg);border:2px solid var(--uxbert-border);border-radius:6px;cursor:pointer;display:flex;font-family:Arial,sans-serif;font-size:18px;font-weight:700;height:40px;justify-content:center;transition:all .2s ease;width:40px}.uxbert-tool-btn.active,.uxbert-tool-btn:hover{background:var(--uxbert-primary);border-color:var(--uxbert-primary);color:#fff}.uxbert-color-picker{display:flex;gap:6px;margin:8px 0}.uxbert-color-option{border:2px solid var(--uxbert-border);border-radius:50%;cursor:pointer;height:32px;transition:transform .2s ease;width:32px}.uxbert-color-option:hover{transform:scale(1.1)}.uxbert-color-option.active{border:3px solid var(--uxbert-text)}.uxbert-canvas-overlay{cursor:crosshair;display:none;left:0;position:absolute;top:0;z-index:var(--uxbert-z-canvas)}.uxbert-canvas-overlay.active{display:block}.uxbert-canvas-overlay.viewport-mode{left:0;position:fixed;top:0}.uxbert-text-input{background:hsla(0,0%,100%,.95);border:2px solid var(--uxbert-primary);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);color:var(--uxbert-text);font-family:Arial,sans-serif;font-size:16px;font-weight:700;min-width:200px;outline:none;padding:8px 12px;z-index:var(--uxbert-z-toolbar)}.uxbert-text-input:focus{border-color:var(--uxbert-primary-hover);box-shadow:0 4px 16px rgba(79,70,229,.3)}.uxbert-text-input::placeholder{color:#9ca3af;font-weight:400}@media (max-width:768px){.uxbert-modal{max-height:95vh;padding:16px;width:95%}.uxbert-toolbar{bottom:90px;left:16px;max-height:60vh;right:16px;width:auto}.uxbert-toolbar-toggle{bottom:16px;height:56px;right:16px;width:56px}.uxbert-fab{font-size:20px;height:48px;width:48px}.uxbert-color-picker,.uxbert-toolbar-tools{justify-content:center}}";
10569
+ var css_248z = ":root{--uxbert-primary:#4f46e5;--uxbert-primary-hover:#4338ca;--uxbert-danger:#ef4444;--uxbert-success:#10b981;--uxbert-bg:#fff;--uxbert-text:#1f2937;--uxbert-border:#e5e7eb;--uxbert-shadow:0 10px 25px rgba(0,0,0,.1);--uxbert-z-button:999999;--uxbert-z-modal:1000000;--uxbert-z-canvas:1000001;--uxbert-z-toolbar:1000002}[data-theme=dark]{--uxbert-bg:#1f2937;--uxbert-text:#f9fafb;--uxbert-border:#374151}.uxbert-fab{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:flex;font-size:24px;height:56px;justify-content:center;position:fixed;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-button)}.uxbert-fab:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-fab.bottom-right{bottom:24px;right:24px}.uxbert-fab.bottom-left{bottom:24px;left:24px}.uxbert-fab.top-right{right:24px;top:24px}.uxbert-fab.top-left{left:24px;top:24px}.uxbert-overlay{align-items:center;animation:fadeIn .3s ease;background:rgba(0,0,0,.5);display:none;height:100%;justify-content:center;left:0;overflow:hidden;position:fixed;top:0;width:100%;z-index:var(--uxbert-z-modal)}.uxbert-overlay.active{display:flex}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.uxbert-modal{animation:slideUp .3s ease;background:var(--uxbert-bg);border-radius:12px;box-shadow:var(--uxbert-shadow);color:var(--uxbert-text);max-height:90vh;max-width:600px;overflow-y:auto;padding:24px;width:90%}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.uxbert-modal-header{align-items:center;display:flex;justify-content:space-between;margin-bottom:20px}.uxbert-modal-title{font-size:20px;font-weight:600;margin:0}.uxbert-modal-close{align-items:center;background:none;border:none;border-radius:4px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:24px;height:32px;justify-content:center;padding:0;width:32px}.uxbert-modal-close:hover{background:var(--uxbert-border)}.uxbert-form-group{margin-bottom:16px}.uxbert-form-label{display:block;font-size:14px;font-weight:500;margin-bottom:8px}.uxbert-capture-mode{display:flex;flex-wrap:wrap;gap:12px}.uxbert-radio-label{align-items:center;border:2px solid var(--uxbert-border);border-radius:8px;cursor:pointer;display:flex;flex:1;gap:8px;min-width:140px;padding:10px 16px;transition:all .2s ease}.uxbert-radio-label:hover{background:rgba(79,70,229,.05);border-color:var(--uxbert-primary)}.uxbert-radio-label input[type=radio]{cursor:pointer;height:18px;margin:0;width:18px}.uxbert-radio-label input[type=radio]:checked{accent-color:var(--uxbert-primary)}.uxbert-radio-label:has(input[type=radio]:checked){background:rgba(79,70,229,.1);border-color:var(--uxbert-primary)}.uxbert-radio-label span{flex:1;font-size:14px;font-weight:500}.uxbert-form-input,.uxbert-form-select,.uxbert-form-textarea{background:var(--uxbert-bg);border:1px solid var(--uxbert-border);border-radius:6px;box-sizing:border-box;color:var(--uxbert-text);font-family:inherit;font-size:14px;padding:10px;width:100%}.uxbert-form-textarea{min-height:100px;resize:vertical}.uxbert-form-input:focus,.uxbert-form-select:focus,.uxbert-form-textarea:focus{border-color:var(--uxbert-primary);outline:none}.uxbert-capture-action{margin:16px 0;text-align:center}.uxbert-capture-action .uxbert-btn{min-width:200px}.uxbert-screenshot-preview{border:1px solid var(--uxbert-border);border-radius:8px;margin:16px 0;overflow:hidden}.uxbert-screenshot-preview img{display:block;width:100%}.uxbert-screenshot-actions{background:var(--uxbert-border);display:flex;gap:8px;padding:12px}.uxbert-btn{align-items:center;border:none;border-radius:6px;cursor:pointer;display:inline-flex;font-size:14px;font-weight:500;gap:8px;padding:10px 20px;transition:all .2s ease}.uxbert-btn-primary{background:var(--uxbert-primary);color:#fff}.uxbert-btn-primary:hover{background:var(--uxbert-primary-hover)}.uxbert-btn-secondary{background:var(--uxbert-border);color:var(--uxbert-text)}.uxbert-btn-secondary:hover{background:#d1d5db}.uxbert-btn-success{background:var(--uxbert-success);color:#fff}.uxbert-btn-success:hover{background:#059669}.uxbert-btn-danger{background:var(--uxbert-danger);color:#fff}.uxbert-btn-danger:hover{background:#dc2626}.uxbert-modal-actions{display:flex;gap:12px;justify-content:flex-end;margin-top:20px}.uxbert-toolbar-toggle{align-items:center;background:var(--uxbert-primary);border:none;border-radius:50%;bottom:24px;box-shadow:var(--uxbert-shadow);color:#fff;cursor:pointer;display:none;font-size:24px;height:56px;justify-content:center;position:fixed;right:24px;transition:all .3s ease;width:56px;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar-toggle.active{display:flex}.uxbert-toolbar-toggle.hidden{opacity:0;pointer-events:none;transform:scale(.8)}.uxbert-toolbar-toggle:hover{background:var(--uxbert-primary-hover);transform:scale(1.1)}.uxbert-toolbar{background:var(--uxbert-bg);border-radius:12px;bottom:24px;box-shadow:var(--uxbert-shadow);display:none;flex-direction:column;gap:8px;max-height:80vh;opacity:0;overflow-y:auto;padding:16px;pointer-events:none;position:fixed;right:24px;transform:translateY(20px);transition:all .3s ease;z-index:var(--uxbert-z-toolbar)}.uxbert-toolbar.active{display:flex}.uxbert-toolbar.expanded{opacity:1;pointer-events:all;transform:translateY(0)}.uxbert-toolbar-header{align-items:center;display:flex;gap:8px;justify-content:space-between;margin-bottom:8px}.uxbert-toolbar-title{flex:1;font-size:14px;font-weight:600}.uxbert-toolbar-exit,.uxbert-toolbar-minimize{align-items:center;background:var(--uxbert-border);border:none;border-radius:6px;color:var(--uxbert-text);cursor:pointer;display:flex;font-size:20px;font-weight:700;height:32px;justify-content:center;transition:all .2s ease;width:32px}.uxbert-toolbar-exit{background:var(--uxbert-danger);color:#fff}.uxbert-toolbar-minimize:hover{background:#d1d5db;transform:scale(1.1)}.uxbert-toolbar-exit:hover{background:#dc2626;transform:scale(1.1)}.uxbert-toolbar-tools{display:flex;flex-wrap:wrap;gap:8px}.uxbert-tool-btn{align-items:center;background:var(--uxbert-bg);border:2px solid var(--uxbert-border);border-radius:6px;cursor:pointer;display:flex;font-family:Arial,sans-serif;font-size:18px;font-weight:700;height:40px;justify-content:center;transition:all .2s ease;width:40px}.uxbert-tool-btn.active,.uxbert-tool-btn:hover{background:var(--uxbert-primary);border-color:var(--uxbert-primary);color:#fff}.uxbert-color-picker{display:flex;gap:6px;margin:8px 0}.uxbert-color-option{border:2px solid var(--uxbert-border);border-radius:50%;cursor:pointer;height:32px;transition:transform .2s ease;width:32px}.uxbert-color-option:hover{transform:scale(1.1)}.uxbert-color-option.active{border:3px solid var(--uxbert-text)}.uxbert-canvas-overlay{cursor:crosshair;display:none;left:0;position:absolute;top:0;z-index:var(--uxbert-z-canvas)}.uxbert-canvas-overlay.active{display:block}.uxbert-canvas-overlay.viewport-mode{left:0;position:fixed;top:0}.uxbert-text-input{background:hsla(0,0%,100%,.95);border:2px solid var(--uxbert-primary);border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.15);color:var(--uxbert-text);font-family:Arial,sans-serif;font-size:16px;font-weight:700;min-width:200px;outline:none;padding:8px 12px;z-index:var(--uxbert-z-toolbar)}.uxbert-text-input:focus{border-color:var(--uxbert-primary-hover);box-shadow:0 4px 16px rgba(79,70,229,.3)}.uxbert-text-input::placeholder{color:#9ca3af;font-weight:400}@media (max-width:768px){.uxbert-modal{max-height:95vh;padding:16px;width:95%}.uxbert-toolbar{bottom:90px;left:16px;max-height:60vh;right:16px;width:auto}.uxbert-toolbar-toggle{bottom:16px;height:56px;right:16px;width:56px}.uxbert-fab{font-size:20px;height:48px;width:48px}.uxbert-color-picker,.uxbert-toolbar-tools{justify-content:center}}";
10263
10570
  styleInject(css_248z);
10264
10571
 
10265
10572
  // Main initialization file
@@ -10291,7 +10598,9 @@ class Reportly {
10291
10598
  this.state = new State();
10292
10599
  this.deviceInfo = new DeviceInfo();
10293
10600
  this.screenshot = new Screenshot();
10294
- this.export = new Export();
10601
+ // Initialize export with n8n config if provided
10602
+ const n8nConfig = userConfig.integrations?.n8n;
10603
+ this.export = new Export(n8nConfig);
10295
10604
  // Initialize UI components
10296
10605
  this.button = new Button(this.config, {
10297
10606
  onClick: () => this.handleButtonClick(),
@@ -10302,6 +10611,7 @@ class Reportly {
10302
10611
  onAnnotate: () => this.startAnnotation(),
10303
10612
  onRetake: () => this.retakeScreenshot(),
10304
10613
  onCapture: () => this.captureWithMode(),
10614
+ onSendToN8n: (issueData) => this.handleN8nSubmit(issueData),
10305
10615
  });
10306
10616
  this.toolbar = new Toolbar({
10307
10617
  onToolChange: (tool) => this.annotation?.setTool(tool),
@@ -10317,6 +10627,10 @@ class Reportly {
10317
10627
  this.modal.create();
10318
10628
  this.toolbar.create();
10319
10629
  this.annotation.createCanvas();
10630
+ // Show/hide n8n button based on configuration
10631
+ if (this.export.isN8nEnabled()) {
10632
+ this.modal.setN8nButtonVisible(true);
10633
+ }
10320
10634
  // Setup keyboard shortcuts
10321
10635
  this.setupKeyboardShortcuts();
10322
10636
  this.isInitialized = true;
@@ -10444,6 +10758,54 @@ class Reportly {
10444
10758
  alert("Failed to export issue. Please try again.");
10445
10759
  }
10446
10760
  }
10761
+ async handleN8nSubmit(issueData) {
10762
+ try {
10763
+ // Create complete issue package
10764
+ const completeIssue = {
10765
+ ...issueData,
10766
+ screenshot: this.state?.getScreenshot() || "",
10767
+ deviceInfo: this.deviceInfo.get(),
10768
+ createdAt: new Date().toISOString(),
10769
+ };
10770
+ // Show loading state
10771
+ const n8nBtn = document.querySelector('#uxbert-n8n-btn');
10772
+ if (n8nBtn) {
10773
+ n8nBtn.disabled = true;
10774
+ n8nBtn.textContent = '⏳ Sending...';
10775
+ }
10776
+ // Send to n8n
10777
+ const response = await this.export?.sendToN8n(completeIssue);
10778
+ if (response?.success) {
10779
+ // Emit event
10780
+ this.state?.emit("issue:sent-to-n8n", completeIssue);
10781
+ // Reset state
10782
+ this.state?.reset();
10783
+ this.annotation?.clear();
10784
+ this.modal?.close();
10785
+ alert("Issue sent to n8n successfully!");
10786
+ console.log("Issue sent to n8n successfully");
10787
+ }
10788
+ else {
10789
+ alert(`Failed to send to n8n: ${response?.message || 'Unknown error'}`);
10790
+ console.error("Failed to send to n8n:", response?.error);
10791
+ }
10792
+ // Reset button state
10793
+ if (n8nBtn) {
10794
+ n8nBtn.disabled = false;
10795
+ n8nBtn.textContent = '🚀 Send to n8n';
10796
+ }
10797
+ }
10798
+ catch (error) {
10799
+ console.error("Failed to send to n8n:", error);
10800
+ alert("Failed to send to n8n. Please try again.");
10801
+ // Reset button state
10802
+ const n8nBtn = document.querySelector('#uxbert-n8n-btn');
10803
+ if (n8nBtn) {
10804
+ n8nBtn.disabled = false;
10805
+ n8nBtn.textContent = '🚀 Send to n8n';
10806
+ }
10807
+ }
10808
+ }
10447
10809
  handleModalClose() {
10448
10810
  this.annotation?.hide();
10449
10811
  this.toolbar?.hide();
@@ -10472,6 +10834,24 @@ class Reportly {
10472
10834
  exportAllIssues() {
10473
10835
  this.export?.exportAllIssues();
10474
10836
  }
10837
+ // n8n Integration API methods
10838
+ configureN8n(webhookUrl, enabled = true, options) {
10839
+ this.export?.configureN8n({
10840
+ webhookUrl,
10841
+ enabled,
10842
+ headers: options?.headers,
10843
+ timeout: options?.timeout
10844
+ });
10845
+ // Update button visibility
10846
+ this.modal?.setN8nButtonVisible(enabled);
10847
+ }
10848
+ enableN8n(enabled) {
10849
+ this.export?.setN8nEnabled(enabled);
10850
+ this.modal?.setN8nButtonVisible(enabled);
10851
+ }
10852
+ testN8nConnection() {
10853
+ return this.export?.testN8nConnection() || Promise.resolve({ success: false, message: 'Not initialized' });
10854
+ }
10475
10855
  on(event, callback) {
10476
10856
  this.state?.on(event, callback);
10477
10857
  }