@fatagnus/convex-feedback 0.1.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/LICENSE +177 -0
- package/README.md +382 -0
- package/dist/convex/agents/bugReportAgent.d.ts +30 -0
- package/dist/convex/agents/bugReportAgent.d.ts.map +1 -0
- package/dist/convex/agents/bugReportAgent.js +243 -0
- package/dist/convex/agents/bugReportAgent.js.map +1 -0
- package/dist/convex/agents/feedbackAgent.d.ts +29 -0
- package/dist/convex/agents/feedbackAgent.d.ts.map +1 -0
- package/dist/convex/agents/feedbackAgent.js +232 -0
- package/dist/convex/agents/feedbackAgent.js.map +1 -0
- package/dist/convex/bugReports.d.ts +49 -0
- package/dist/convex/bugReports.d.ts.map +1 -0
- package/dist/convex/bugReports.js +321 -0
- package/dist/convex/bugReports.js.map +1 -0
- package/dist/convex/convex.config.d.ts +3 -0
- package/dist/convex/convex.config.d.ts.map +1 -0
- package/dist/convex/convex.config.js +6 -0
- package/dist/convex/convex.config.js.map +1 -0
- package/dist/convex/emails/bugReportEmails.d.ts +16 -0
- package/dist/convex/emails/bugReportEmails.d.ts.map +1 -0
- package/dist/convex/emails/bugReportEmails.js +403 -0
- package/dist/convex/emails/bugReportEmails.js.map +1 -0
- package/dist/convex/emails/feedbackEmails.d.ts +16 -0
- package/dist/convex/emails/feedbackEmails.d.ts.map +1 -0
- package/dist/convex/emails/feedbackEmails.js +389 -0
- package/dist/convex/emails/feedbackEmails.js.map +1 -0
- package/dist/convex/feedback.d.ts +49 -0
- package/dist/convex/feedback.d.ts.map +1 -0
- package/dist/convex/feedback.js +327 -0
- package/dist/convex/feedback.js.map +1 -0
- package/dist/convex/index.d.ts +10 -0
- package/dist/convex/index.d.ts.map +1 -0
- package/dist/convex/index.js +12 -0
- package/dist/convex/index.js.map +1 -0
- package/dist/convex/schema.d.ts +200 -0
- package/dist/convex/schema.d.ts.map +1 -0
- package/dist/convex/schema.js +150 -0
- package/dist/convex/schema.js.map +1 -0
- package/dist/convex/supportTeams.d.ts +29 -0
- package/dist/convex/supportTeams.d.ts.map +1 -0
- package/dist/convex/supportTeams.js +159 -0
- package/dist/convex/supportTeams.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/react/BugReportButton.d.ts +70 -0
- package/dist/react/BugReportButton.d.ts.map +1 -0
- package/dist/react/BugReportButton.js +371 -0
- package/dist/react/BugReportButton.js.map +1 -0
- package/dist/react/BugReportContext.d.ts +59 -0
- package/dist/react/BugReportContext.d.ts.map +1 -0
- package/dist/react/BugReportContext.js +107 -0
- package/dist/react/BugReportContext.js.map +1 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +36 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +101 -0
- package/src/convex/agents/bugReportAgent.ts +277 -0
- package/src/convex/agents/feedbackAgent.ts +264 -0
- package/src/convex/bugReports.ts +350 -0
- package/src/convex/convex.config.ts +7 -0
- package/src/convex/emails/bugReportEmails.ts +479 -0
- package/src/convex/emails/feedbackEmails.ts +465 -0
- package/src/convex/feedback.ts +356 -0
- package/src/convex/index.ts +28 -0
- package/src/convex/schema.ts +207 -0
- package/src/convex/supportTeams.ts +179 -0
- package/src/index.ts +77 -0
- package/src/react/BugReportButton.tsx +755 -0
- package/src/react/BugReportContext.tsx +146 -0
- package/src/react/index.ts +46 -0
- package/src/types.ts +93 -0
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ActionIcon,
|
|
4
|
+
Modal,
|
|
5
|
+
TextInput,
|
|
6
|
+
Textarea,
|
|
7
|
+
Select,
|
|
8
|
+
Button,
|
|
9
|
+
Group,
|
|
10
|
+
Stack,
|
|
11
|
+
Text,
|
|
12
|
+
Image,
|
|
13
|
+
Tooltip,
|
|
14
|
+
Affix,
|
|
15
|
+
Transition,
|
|
16
|
+
Paper,
|
|
17
|
+
Badge,
|
|
18
|
+
LoadingOverlay,
|
|
19
|
+
SegmentedControl,
|
|
20
|
+
FileButton,
|
|
21
|
+
} from '@mantine/core';
|
|
22
|
+
import { useForm } from '@mantine/form';
|
|
23
|
+
import { useDisclosure } from '@mantine/hooks';
|
|
24
|
+
import { notifications } from '@mantine/notifications';
|
|
25
|
+
import * as TablerIcons from '@tabler/icons-react';
|
|
26
|
+
|
|
27
|
+
const IconBug = TablerIcons.IconBug as React.FC<{ size?: number }>;
|
|
28
|
+
const IconCamera = TablerIcons.IconCamera as React.FC<{ size?: number }>;
|
|
29
|
+
const IconX = TablerIcons.IconX as React.FC<{ size?: number }>;
|
|
30
|
+
const IconCheck = TablerIcons.IconCheck as React.FC<{ size?: number }>;
|
|
31
|
+
const IconMessagePlus = TablerIcons.IconMessagePlus as React.FC<{ size?: number }>;
|
|
32
|
+
const IconUpload = TablerIcons.IconUpload as React.FC<{ size?: number }>;
|
|
33
|
+
import { useMutation } from 'convex/react';
|
|
34
|
+
import html2canvas from 'html2canvas';
|
|
35
|
+
import { useBugReportContext } from './BugReportContext';
|
|
36
|
+
import type { FunctionReference } from 'convex/server';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Props for the BugReportButton component
|
|
40
|
+
*/
|
|
41
|
+
export interface BugReportButtonProps {
|
|
42
|
+
/** Reporter type: 'staff' for internal users, 'customer' for external users */
|
|
43
|
+
reporterType: 'staff' | 'customer';
|
|
44
|
+
/** Unique identifier for the reporter */
|
|
45
|
+
reporterId: string;
|
|
46
|
+
/** Reporter's email address */
|
|
47
|
+
reporterEmail: string;
|
|
48
|
+
/** Reporter's display name */
|
|
49
|
+
reporterName: string;
|
|
50
|
+
/** Convex API reference for bug reports */
|
|
51
|
+
bugReportApi: {
|
|
52
|
+
create: FunctionReference<'mutation'>;
|
|
53
|
+
generateUploadUrl: FunctionReference<'mutation'>;
|
|
54
|
+
};
|
|
55
|
+
/** Convex API reference for feedback */
|
|
56
|
+
feedbackApi: {
|
|
57
|
+
create: FunctionReference<'mutation'>;
|
|
58
|
+
generateUploadUrl: FunctionReference<'mutation'>;
|
|
59
|
+
};
|
|
60
|
+
/** Position of the floating button */
|
|
61
|
+
position?: { bottom?: number; right?: number; top?: number; left?: number };
|
|
62
|
+
/** Button color (default: red) */
|
|
63
|
+
buttonColor?: string;
|
|
64
|
+
/** Show feedback tab in addition to bug reports */
|
|
65
|
+
showFeedback?: boolean;
|
|
66
|
+
/** Callback when submission succeeds */
|
|
67
|
+
onSuccess?: (type: 'bug' | 'feedback') => void;
|
|
68
|
+
/** Callback when submission fails */
|
|
69
|
+
onError?: (error: Error, type: 'bug' | 'feedback') => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface BrowserInfo {
|
|
73
|
+
userAgent: string;
|
|
74
|
+
platform: string;
|
|
75
|
+
language: string;
|
|
76
|
+
cookiesEnabled: boolean;
|
|
77
|
+
onLine: boolean;
|
|
78
|
+
screenWidth: number;
|
|
79
|
+
screenHeight: number;
|
|
80
|
+
colorDepth: number;
|
|
81
|
+
pixelRatio: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type FormMode = 'bug' | 'feedback';
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Floating action button for submitting bug reports and feedback.
|
|
88
|
+
* Captures browser diagnostics, console errors, and optional screenshots.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```tsx
|
|
92
|
+
* import { BugReportButton } from '@convex-dev/feedback/react';
|
|
93
|
+
* import { api } from './convex/_generated/api';
|
|
94
|
+
*
|
|
95
|
+
* function App() {
|
|
96
|
+
* return (
|
|
97
|
+
* <BugReportButton
|
|
98
|
+
* reporterType="staff"
|
|
99
|
+
* reporterId={user.id}
|
|
100
|
+
* reporterEmail={user.email}
|
|
101
|
+
* reporterName={user.name}
|
|
102
|
+
* bugReportApi={{
|
|
103
|
+
* create: api.feedback.bugReports.create,
|
|
104
|
+
* generateUploadUrl: api.feedback.bugReports.generateUploadUrl,
|
|
105
|
+
* }}
|
|
106
|
+
* feedbackApi={{
|
|
107
|
+
* create: api.feedback.feedback.create,
|
|
108
|
+
* generateUploadUrl: api.feedback.feedback.generateUploadUrl,
|
|
109
|
+
* }}
|
|
110
|
+
* />
|
|
111
|
+
* );
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function BugReportButton({
|
|
116
|
+
reporterType,
|
|
117
|
+
reporterId,
|
|
118
|
+
reporterEmail,
|
|
119
|
+
reporterName,
|
|
120
|
+
bugReportApi,
|
|
121
|
+
feedbackApi,
|
|
122
|
+
position = { bottom: 20, right: 20 },
|
|
123
|
+
buttonColor = 'red',
|
|
124
|
+
showFeedback = true,
|
|
125
|
+
onSuccess,
|
|
126
|
+
onError,
|
|
127
|
+
}: BugReportButtonProps) {
|
|
128
|
+
const [opened, { open, close }] = useDisclosure(false);
|
|
129
|
+
const [formMode, setFormMode] = useState<FormMode>('bug');
|
|
130
|
+
const [screenshot, setScreenshot] = useState<string | null>(null);
|
|
131
|
+
const [screenshotBlob, setScreenshotBlob] = useState<Blob | null>(null);
|
|
132
|
+
const [isCapturing, setIsCapturing] = useState(false);
|
|
133
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
134
|
+
|
|
135
|
+
const { getConsoleErrors, clearConsoleErrors } = useBugReportContext();
|
|
136
|
+
const createBugReport = useMutation(bugReportApi.create);
|
|
137
|
+
const createFeedback = useMutation(feedbackApi.create);
|
|
138
|
+
const generateBugUploadUrl = useMutation(bugReportApi.generateUploadUrl);
|
|
139
|
+
const generateFeedbackUploadUrl = useMutation(feedbackApi.generateUploadUrl);
|
|
140
|
+
|
|
141
|
+
// Bug report form
|
|
142
|
+
const bugForm = useForm({
|
|
143
|
+
initialValues: {
|
|
144
|
+
title: '',
|
|
145
|
+
description: '',
|
|
146
|
+
severity: 'medium' as 'low' | 'medium' | 'high' | 'critical',
|
|
147
|
+
},
|
|
148
|
+
validate: {
|
|
149
|
+
title: (value) =>
|
|
150
|
+
value.trim().length < 5 ? 'Title must be at least 5 characters' : null,
|
|
151
|
+
description: (value) =>
|
|
152
|
+
value.trim().length < 10
|
|
153
|
+
? 'Please provide more details (at least 10 characters)'
|
|
154
|
+
: null,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Feedback form
|
|
159
|
+
const feedbackForm = useForm({
|
|
160
|
+
initialValues: {
|
|
161
|
+
title: '',
|
|
162
|
+
description: '',
|
|
163
|
+
type: 'feature_request' as 'feature_request' | 'change_request' | 'general',
|
|
164
|
+
priority: 'important' as 'nice_to_have' | 'important' | 'critical',
|
|
165
|
+
},
|
|
166
|
+
validate: {
|
|
167
|
+
title: (value) =>
|
|
168
|
+
value.trim().length < 5 ? 'Title must be at least 5 characters' : null,
|
|
169
|
+
description: (value) =>
|
|
170
|
+
value.trim().length < 10
|
|
171
|
+
? 'Please provide more details (at least 10 characters)'
|
|
172
|
+
: null,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const getBrowserInfo = useCallback((): BrowserInfo => {
|
|
177
|
+
return {
|
|
178
|
+
userAgent: navigator.userAgent,
|
|
179
|
+
platform: navigator.platform,
|
|
180
|
+
language: navigator.language,
|
|
181
|
+
cookiesEnabled: navigator.cookieEnabled,
|
|
182
|
+
onLine: navigator.onLine,
|
|
183
|
+
screenWidth: window.screen.width,
|
|
184
|
+
screenHeight: window.screen.height,
|
|
185
|
+
colorDepth: window.screen.colorDepth,
|
|
186
|
+
pixelRatio: window.devicePixelRatio,
|
|
187
|
+
};
|
|
188
|
+
}, []);
|
|
189
|
+
|
|
190
|
+
const captureScreenshot = useCallback(async () => {
|
|
191
|
+
setIsCapturing(true);
|
|
192
|
+
try {
|
|
193
|
+
// Hide the modal temporarily for screenshot
|
|
194
|
+
const modalElement = document.querySelector('[data-bug-report-modal]');
|
|
195
|
+
if (modalElement) {
|
|
196
|
+
(modalElement as HTMLElement).style.visibility = 'hidden';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Small delay to ensure modal is hidden
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
201
|
+
|
|
202
|
+
const canvas = await html2canvas(document.body, {
|
|
203
|
+
logging: false,
|
|
204
|
+
useCORS: true,
|
|
205
|
+
allowTaint: true,
|
|
206
|
+
scale: 1,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Show modal again
|
|
210
|
+
if (modalElement) {
|
|
211
|
+
(modalElement as HTMLElement).style.visibility = 'visible';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
215
|
+
setScreenshot(dataUrl);
|
|
216
|
+
|
|
217
|
+
// Convert to blob for upload
|
|
218
|
+
canvas.toBlob((blob) => {
|
|
219
|
+
if (blob) {
|
|
220
|
+
setScreenshotBlob(blob);
|
|
221
|
+
}
|
|
222
|
+
}, 'image/png');
|
|
223
|
+
|
|
224
|
+
notifications.show({
|
|
225
|
+
title: 'Screenshot captured',
|
|
226
|
+
message: 'Screenshot has been attached to your report',
|
|
227
|
+
color: 'green',
|
|
228
|
+
icon: <IconCheck size={16} />,
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('Failed to capture screenshot:', error);
|
|
232
|
+
notifications.show({
|
|
233
|
+
title: 'Screenshot failed',
|
|
234
|
+
message: 'Could not capture screenshot. You can still submit the report.',
|
|
235
|
+
color: 'yellow',
|
|
236
|
+
});
|
|
237
|
+
} finally {
|
|
238
|
+
setIsCapturing(false);
|
|
239
|
+
}
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
const removeScreenshot = useCallback(() => {
|
|
243
|
+
setScreenshot(null);
|
|
244
|
+
setScreenshotBlob(null);
|
|
245
|
+
}, []);
|
|
246
|
+
|
|
247
|
+
const handleFileUpload = useCallback((file: File | null) => {
|
|
248
|
+
if (!file) return;
|
|
249
|
+
|
|
250
|
+
// Validate file type
|
|
251
|
+
if (!file.type.startsWith('image/')) {
|
|
252
|
+
notifications.show({
|
|
253
|
+
title: 'Invalid file type',
|
|
254
|
+
message: 'Please upload an image file (PNG, JPG, GIF, etc.)',
|
|
255
|
+
color: 'red',
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Validate file size (max 10MB)
|
|
261
|
+
const maxSize = 10 * 1024 * 1024;
|
|
262
|
+
if (file.size > maxSize) {
|
|
263
|
+
notifications.show({
|
|
264
|
+
title: 'File too large',
|
|
265
|
+
message: 'Please upload an image smaller than 10MB',
|
|
266
|
+
color: 'red',
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Read the file and set as screenshot
|
|
272
|
+
const reader = new FileReader();
|
|
273
|
+
reader.onload = (e) => {
|
|
274
|
+
const dataUrl = e.target?.result as string;
|
|
275
|
+
setScreenshot(dataUrl);
|
|
276
|
+
setScreenshotBlob(file);
|
|
277
|
+
notifications.show({
|
|
278
|
+
title: 'Screenshot uploaded',
|
|
279
|
+
message: 'Screenshot has been attached to your report',
|
|
280
|
+
color: 'green',
|
|
281
|
+
icon: <IconCheck size={16} />,
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
reader.onerror = () => {
|
|
285
|
+
notifications.show({
|
|
286
|
+
title: 'Upload failed',
|
|
287
|
+
message: 'Could not read the image file. Please try again.',
|
|
288
|
+
color: 'red',
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
reader.readAsDataURL(file);
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
const handleBugSubmit = async (values: typeof bugForm.values) => {
|
|
295
|
+
setIsSubmitting(true);
|
|
296
|
+
try {
|
|
297
|
+
// Upload screenshot if exists
|
|
298
|
+
let screenshotStorageId: string | undefined;
|
|
299
|
+
if (screenshotBlob) {
|
|
300
|
+
const uploadUrl = await generateBugUploadUrl({});
|
|
301
|
+
const response = await fetch(uploadUrl, {
|
|
302
|
+
method: 'POST',
|
|
303
|
+
headers: { 'Content-Type': screenshotBlob.type || 'image/png' },
|
|
304
|
+
body: screenshotBlob,
|
|
305
|
+
});
|
|
306
|
+
const { storageId } = await response.json();
|
|
307
|
+
screenshotStorageId = storageId;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Get console errors
|
|
311
|
+
const consoleErrors = getConsoleErrors();
|
|
312
|
+
|
|
313
|
+
// Create the report
|
|
314
|
+
await createBugReport({
|
|
315
|
+
title: values.title,
|
|
316
|
+
description: values.description,
|
|
317
|
+
severity: values.severity,
|
|
318
|
+
reporterType,
|
|
319
|
+
reporterId,
|
|
320
|
+
reporterEmail,
|
|
321
|
+
reporterName,
|
|
322
|
+
url: window.location.href,
|
|
323
|
+
route: window.location.pathname,
|
|
324
|
+
browserInfo: JSON.stringify(getBrowserInfo()),
|
|
325
|
+
consoleErrors:
|
|
326
|
+
consoleErrors.length > 0 ? JSON.stringify(consoleErrors) : undefined,
|
|
327
|
+
screenshotStorageId,
|
|
328
|
+
viewportWidth: window.innerWidth,
|
|
329
|
+
viewportHeight: window.innerHeight,
|
|
330
|
+
networkState: navigator.onLine ? 'online' : 'offline',
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Clear console errors after successful submission
|
|
334
|
+
clearConsoleErrors();
|
|
335
|
+
|
|
336
|
+
notifications.show({
|
|
337
|
+
title: 'Bug report submitted',
|
|
338
|
+
message: 'Thank you for your feedback! We will look into this issue.',
|
|
339
|
+
color: 'green',
|
|
340
|
+
icon: <IconCheck size={16} />,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
onSuccess?.('bug');
|
|
344
|
+
|
|
345
|
+
// Reset form and close modal
|
|
346
|
+
bugForm.reset();
|
|
347
|
+
setScreenshot(null);
|
|
348
|
+
setScreenshotBlob(null);
|
|
349
|
+
close();
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('Failed to submit bug report:', error);
|
|
352
|
+
notifications.show({
|
|
353
|
+
title: 'Submission failed',
|
|
354
|
+
message: 'Could not submit bug report. Please try again.',
|
|
355
|
+
color: 'red',
|
|
356
|
+
});
|
|
357
|
+
onError?.(error instanceof Error ? error : new Error(String(error)), 'bug');
|
|
358
|
+
} finally {
|
|
359
|
+
setIsSubmitting(false);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const handleFeedbackSubmit = async (values: typeof feedbackForm.values) => {
|
|
364
|
+
setIsSubmitting(true);
|
|
365
|
+
try {
|
|
366
|
+
// Upload screenshot if exists
|
|
367
|
+
let screenshotStorageId: string | undefined;
|
|
368
|
+
if (screenshotBlob) {
|
|
369
|
+
const uploadUrl = await generateFeedbackUploadUrl({});
|
|
370
|
+
const response = await fetch(uploadUrl, {
|
|
371
|
+
method: 'POST',
|
|
372
|
+
headers: { 'Content-Type': screenshotBlob.type || 'image/png' },
|
|
373
|
+
body: screenshotBlob,
|
|
374
|
+
});
|
|
375
|
+
const { storageId } = await response.json();
|
|
376
|
+
screenshotStorageId = storageId;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Get console errors
|
|
380
|
+
const consoleErrors = getConsoleErrors();
|
|
381
|
+
|
|
382
|
+
// Create the feedback
|
|
383
|
+
await createFeedback({
|
|
384
|
+
type: values.type,
|
|
385
|
+
title: values.title,
|
|
386
|
+
description: values.description,
|
|
387
|
+
priority: values.priority,
|
|
388
|
+
reporterType,
|
|
389
|
+
reporterId,
|
|
390
|
+
reporterEmail,
|
|
391
|
+
reporterName,
|
|
392
|
+
url: window.location.href,
|
|
393
|
+
route: window.location.pathname,
|
|
394
|
+
browserInfo: JSON.stringify(getBrowserInfo()),
|
|
395
|
+
consoleErrors:
|
|
396
|
+
consoleErrors.length > 0 ? JSON.stringify(consoleErrors) : undefined,
|
|
397
|
+
screenshotStorageId,
|
|
398
|
+
viewportWidth: window.innerWidth,
|
|
399
|
+
viewportHeight: window.innerHeight,
|
|
400
|
+
networkState: navigator.onLine ? 'online' : 'offline',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Clear console errors after successful submission
|
|
404
|
+
clearConsoleErrors();
|
|
405
|
+
|
|
406
|
+
notifications.show({
|
|
407
|
+
title: 'Feedback submitted',
|
|
408
|
+
message: 'Thank you for your feedback! We appreciate your input.',
|
|
409
|
+
color: 'green',
|
|
410
|
+
icon: <IconCheck size={16} />,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
onSuccess?.('feedback');
|
|
414
|
+
|
|
415
|
+
// Reset form and close modal
|
|
416
|
+
feedbackForm.reset();
|
|
417
|
+
setScreenshot(null);
|
|
418
|
+
setScreenshotBlob(null);
|
|
419
|
+
close();
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error('Failed to submit feedback:', error);
|
|
422
|
+
notifications.show({
|
|
423
|
+
title: 'Submission failed',
|
|
424
|
+
message: 'Could not submit feedback. Please try again.',
|
|
425
|
+
color: 'red',
|
|
426
|
+
});
|
|
427
|
+
onError?.(error instanceof Error ? error : new Error(String(error)), 'feedback');
|
|
428
|
+
} finally {
|
|
429
|
+
setIsSubmitting(false);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const handleOpen = () => {
|
|
434
|
+
bugForm.reset();
|
|
435
|
+
feedbackForm.reset();
|
|
436
|
+
setScreenshot(null);
|
|
437
|
+
setScreenshotBlob(null);
|
|
438
|
+
setFormMode('bug');
|
|
439
|
+
open();
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const handleModeChange = (mode: string) => {
|
|
443
|
+
setFormMode(mode as FormMode);
|
|
444
|
+
setScreenshot(null);
|
|
445
|
+
setScreenshotBlob(null);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<>
|
|
450
|
+
{/* Floating Action Button */}
|
|
451
|
+
<Affix position={position}>
|
|
452
|
+
<Transition transition="slide-up" mounted={true}>
|
|
453
|
+
{(transitionStyles) => (
|
|
454
|
+
<Tooltip label="Report a bug" position="left" withArrow>
|
|
455
|
+
<ActionIcon
|
|
456
|
+
style={transitionStyles}
|
|
457
|
+
variant="filled"
|
|
458
|
+
color={buttonColor}
|
|
459
|
+
size="xl"
|
|
460
|
+
radius="xl"
|
|
461
|
+
onClick={handleOpen}
|
|
462
|
+
aria-label="Report a bug"
|
|
463
|
+
>
|
|
464
|
+
<IconBug size={24} />
|
|
465
|
+
</ActionIcon>
|
|
466
|
+
</Tooltip>
|
|
467
|
+
)}
|
|
468
|
+
</Transition>
|
|
469
|
+
</Affix>
|
|
470
|
+
|
|
471
|
+
{/* Bug Report / Feedback Modal */}
|
|
472
|
+
<Modal
|
|
473
|
+
opened={opened}
|
|
474
|
+
onClose={close}
|
|
475
|
+
title={
|
|
476
|
+
<Group gap="xs">
|
|
477
|
+
{formMode === 'bug' ? (
|
|
478
|
+
<IconBug size={20} />
|
|
479
|
+
) : (
|
|
480
|
+
<IconMessagePlus size={20} />
|
|
481
|
+
)}
|
|
482
|
+
<Text fw={600}>
|
|
483
|
+
{formMode === 'bug' ? 'Report a Bug' : 'Submit Feedback'}
|
|
484
|
+
</Text>
|
|
485
|
+
</Group>
|
|
486
|
+
}
|
|
487
|
+
size="md"
|
|
488
|
+
data-bug-report-modal
|
|
489
|
+
>
|
|
490
|
+
<LoadingOverlay visible={isSubmitting} />
|
|
491
|
+
<Stack gap="md">
|
|
492
|
+
{/* Mode Switcher */}
|
|
493
|
+
{showFeedback && (
|
|
494
|
+
<SegmentedControl
|
|
495
|
+
value={formMode}
|
|
496
|
+
onChange={handleModeChange}
|
|
497
|
+
data={[
|
|
498
|
+
{ value: 'bug', label: 'Bug Report' },
|
|
499
|
+
{ value: 'feedback', label: 'Feedback' },
|
|
500
|
+
]}
|
|
501
|
+
fullWidth
|
|
502
|
+
/>
|
|
503
|
+
)}
|
|
504
|
+
|
|
505
|
+
{formMode === 'bug' ? (
|
|
506
|
+
<form onSubmit={bugForm.onSubmit(handleBugSubmit)}>
|
|
507
|
+
<Stack gap="md">
|
|
508
|
+
<TextInput
|
|
509
|
+
label="Title"
|
|
510
|
+
placeholder="Brief summary of the issue"
|
|
511
|
+
required
|
|
512
|
+
{...bugForm.getInputProps('title')}
|
|
513
|
+
/>
|
|
514
|
+
|
|
515
|
+
<Textarea
|
|
516
|
+
label="Description"
|
|
517
|
+
placeholder="What happened? What were you trying to do? What did you expect to happen?"
|
|
518
|
+
required
|
|
519
|
+
minRows={4}
|
|
520
|
+
{...bugForm.getInputProps('description')}
|
|
521
|
+
/>
|
|
522
|
+
|
|
523
|
+
<Select
|
|
524
|
+
label="Severity"
|
|
525
|
+
placeholder="How severe is this issue?"
|
|
526
|
+
data={[
|
|
527
|
+
{ value: 'low', label: 'Low - Minor inconvenience' },
|
|
528
|
+
{ value: 'medium', label: 'Medium - Affects workflow' },
|
|
529
|
+
{ value: 'high', label: 'High - Major functionality broken' },
|
|
530
|
+
{ value: 'critical', label: 'Critical - System unusable' },
|
|
531
|
+
]}
|
|
532
|
+
{...bugForm.getInputProps('severity')}
|
|
533
|
+
/>
|
|
534
|
+
|
|
535
|
+
{/* Screenshot Section */}
|
|
536
|
+
<Stack gap="xs">
|
|
537
|
+
<Group justify="space-between">
|
|
538
|
+
<Text size="sm" fw={500}>
|
|
539
|
+
Screenshot
|
|
540
|
+
</Text>
|
|
541
|
+
{screenshot ? (
|
|
542
|
+
<Button
|
|
543
|
+
variant="subtle"
|
|
544
|
+
color="red"
|
|
545
|
+
size="xs"
|
|
546
|
+
leftSection={<IconX size={14} />}
|
|
547
|
+
onClick={removeScreenshot}
|
|
548
|
+
>
|
|
549
|
+
Remove
|
|
550
|
+
</Button>
|
|
551
|
+
) : (
|
|
552
|
+
<Group gap="xs">
|
|
553
|
+
<Button
|
|
554
|
+
variant="light"
|
|
555
|
+
size="xs"
|
|
556
|
+
leftSection={<IconCamera size={14} />}
|
|
557
|
+
onClick={captureScreenshot}
|
|
558
|
+
loading={isCapturing}
|
|
559
|
+
>
|
|
560
|
+
Capture
|
|
561
|
+
</Button>
|
|
562
|
+
<FileButton onChange={handleFileUpload} accept="image/*">
|
|
563
|
+
{(props) => (
|
|
564
|
+
<Button
|
|
565
|
+
{...props}
|
|
566
|
+
variant="light"
|
|
567
|
+
size="xs"
|
|
568
|
+
leftSection={<IconUpload size={14} />}
|
|
569
|
+
>
|
|
570
|
+
Upload
|
|
571
|
+
</Button>
|
|
572
|
+
)}
|
|
573
|
+
</FileButton>
|
|
574
|
+
</Group>
|
|
575
|
+
)}
|
|
576
|
+
</Group>
|
|
577
|
+
{screenshot && (
|
|
578
|
+
<Paper withBorder p="xs" radius="md">
|
|
579
|
+
<Image
|
|
580
|
+
src={screenshot}
|
|
581
|
+
alt="Bug screenshot"
|
|
582
|
+
radius="md"
|
|
583
|
+
fit="contain"
|
|
584
|
+
h={150}
|
|
585
|
+
/>
|
|
586
|
+
</Paper>
|
|
587
|
+
)}
|
|
588
|
+
</Stack>
|
|
589
|
+
|
|
590
|
+
{/* Auto-captured Info Badge */}
|
|
591
|
+
<Paper withBorder p="xs" radius="md">
|
|
592
|
+
<Group gap="xs">
|
|
593
|
+
<Badge size="xs" variant="outline" color="gray">
|
|
594
|
+
Auto-captured
|
|
595
|
+
</Badge>
|
|
596
|
+
<Text size="xs" c="dimmed">
|
|
597
|
+
Browser info, URL, viewport size, and console errors will be
|
|
598
|
+
included automatically.
|
|
599
|
+
</Text>
|
|
600
|
+
</Group>
|
|
601
|
+
</Paper>
|
|
602
|
+
|
|
603
|
+
{/* Submit Buttons */}
|
|
604
|
+
<Group justify="flex-end" mt="md">
|
|
605
|
+
<Button variant="subtle" onClick={close} disabled={isSubmitting}>
|
|
606
|
+
Cancel
|
|
607
|
+
</Button>
|
|
608
|
+
<Button type="submit" loading={isSubmitting}>
|
|
609
|
+
Submit Report
|
|
610
|
+
</Button>
|
|
611
|
+
</Group>
|
|
612
|
+
</Stack>
|
|
613
|
+
</form>
|
|
614
|
+
) : (
|
|
615
|
+
<form onSubmit={feedbackForm.onSubmit(handleFeedbackSubmit)}>
|
|
616
|
+
<Stack gap="md">
|
|
617
|
+
<Select
|
|
618
|
+
label="Feedback Type"
|
|
619
|
+
placeholder="What kind of feedback is this?"
|
|
620
|
+
data={[
|
|
621
|
+
{
|
|
622
|
+
value: 'feature_request',
|
|
623
|
+
label: 'Feature Request - Idea for new functionality',
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
value: 'change_request',
|
|
627
|
+
label: 'Change Request - Modify existing feature',
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
value: 'general',
|
|
631
|
+
label: 'General Feedback - Comments or suggestions',
|
|
632
|
+
},
|
|
633
|
+
]}
|
|
634
|
+
{...feedbackForm.getInputProps('type')}
|
|
635
|
+
/>
|
|
636
|
+
|
|
637
|
+
<TextInput
|
|
638
|
+
label="Title"
|
|
639
|
+
placeholder="Brief summary of your feedback"
|
|
640
|
+
required
|
|
641
|
+
{...feedbackForm.getInputProps('title')}
|
|
642
|
+
/>
|
|
643
|
+
|
|
644
|
+
<Textarea
|
|
645
|
+
label="Description"
|
|
646
|
+
placeholder="Describe your idea or suggestion in detail. What problem does it solve? How would it improve your workflow?"
|
|
647
|
+
required
|
|
648
|
+
minRows={4}
|
|
649
|
+
{...feedbackForm.getInputProps('description')}
|
|
650
|
+
/>
|
|
651
|
+
|
|
652
|
+
<Select
|
|
653
|
+
label="Priority"
|
|
654
|
+
placeholder="How important is this to you?"
|
|
655
|
+
data={[
|
|
656
|
+
{
|
|
657
|
+
value: 'nice_to_have',
|
|
658
|
+
label: 'Nice to Have - Would be a welcome addition',
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
value: 'important',
|
|
662
|
+
label: 'Important - Would significantly improve my work',
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
value: 'critical',
|
|
666
|
+
label: 'Critical - Blocking my workflow',
|
|
667
|
+
},
|
|
668
|
+
]}
|
|
669
|
+
{...feedbackForm.getInputProps('priority')}
|
|
670
|
+
/>
|
|
671
|
+
|
|
672
|
+
{/* Screenshot Section */}
|
|
673
|
+
<Stack gap="xs">
|
|
674
|
+
<Group justify="space-between">
|
|
675
|
+
<Text size="sm" fw={500}>
|
|
676
|
+
Screenshot (optional)
|
|
677
|
+
</Text>
|
|
678
|
+
{screenshot ? (
|
|
679
|
+
<Button
|
|
680
|
+
variant="subtle"
|
|
681
|
+
color="red"
|
|
682
|
+
size="xs"
|
|
683
|
+
leftSection={<IconX size={14} />}
|
|
684
|
+
onClick={removeScreenshot}
|
|
685
|
+
>
|
|
686
|
+
Remove
|
|
687
|
+
</Button>
|
|
688
|
+
) : (
|
|
689
|
+
<Group gap="xs">
|
|
690
|
+
<Button
|
|
691
|
+
variant="light"
|
|
692
|
+
size="xs"
|
|
693
|
+
leftSection={<IconCamera size={14} />}
|
|
694
|
+
onClick={captureScreenshot}
|
|
695
|
+
loading={isCapturing}
|
|
696
|
+
>
|
|
697
|
+
Capture
|
|
698
|
+
</Button>
|
|
699
|
+
<FileButton onChange={handleFileUpload} accept="image/*">
|
|
700
|
+
{(props) => (
|
|
701
|
+
<Button
|
|
702
|
+
{...props}
|
|
703
|
+
variant="light"
|
|
704
|
+
size="xs"
|
|
705
|
+
leftSection={<IconUpload size={14} />}
|
|
706
|
+
>
|
|
707
|
+
Upload
|
|
708
|
+
</Button>
|
|
709
|
+
)}
|
|
710
|
+
</FileButton>
|
|
711
|
+
</Group>
|
|
712
|
+
)}
|
|
713
|
+
</Group>
|
|
714
|
+
{screenshot && (
|
|
715
|
+
<Paper withBorder p="xs" radius="md">
|
|
716
|
+
<Image
|
|
717
|
+
src={screenshot}
|
|
718
|
+
alt="Feedback screenshot"
|
|
719
|
+
radius="md"
|
|
720
|
+
fit="contain"
|
|
721
|
+
h={150}
|
|
722
|
+
/>
|
|
723
|
+
</Paper>
|
|
724
|
+
)}
|
|
725
|
+
</Stack>
|
|
726
|
+
|
|
727
|
+
{/* Auto-captured Info Badge */}
|
|
728
|
+
<Paper withBorder p="xs" radius="md">
|
|
729
|
+
<Group gap="xs">
|
|
730
|
+
<Badge size="xs" variant="outline" color="gray">
|
|
731
|
+
Auto-captured
|
|
732
|
+
</Badge>
|
|
733
|
+
<Text size="xs" c="dimmed">
|
|
734
|
+
Browser info and page context will be included automatically.
|
|
735
|
+
</Text>
|
|
736
|
+
</Group>
|
|
737
|
+
</Paper>
|
|
738
|
+
|
|
739
|
+
{/* Submit Buttons */}
|
|
740
|
+
<Group justify="flex-end" mt="md">
|
|
741
|
+
<Button variant="subtle" onClick={close} disabled={isSubmitting}>
|
|
742
|
+
Cancel
|
|
743
|
+
</Button>
|
|
744
|
+
<Button type="submit" loading={isSubmitting} color="teal">
|
|
745
|
+
Submit Feedback
|
|
746
|
+
</Button>
|
|
747
|
+
</Group>
|
|
748
|
+
</Stack>
|
|
749
|
+
</form>
|
|
750
|
+
)}
|
|
751
|
+
</Stack>
|
|
752
|
+
</Modal>
|
|
753
|
+
</>
|
|
754
|
+
);
|
|
755
|
+
}
|