import { useState, useCallback } from 'react';
import {
Button,
Modal,
TextInput,
Textarea,
Select,
Stack,
Text,
Group,
Paper,
Badge,
LoadingOverlay,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { useDisclosure } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import * as TablerIcons from '@tabler/icons-react';
import { useMutation } from 'convex/react';
import type { FunctionReference } from 'convex/server';
const IconBug = TablerIcons.IconBug as React.FC<{ size?: number }>;
const IconCheck = TablerIcons.IconCheck as React.FC<{ size?: number }>;
/**
* Props for the ErrorBugReportButton component
*/
export interface ErrorBugReportButtonProps {
/** The error to report */
error: Error;
/** Reporter type: 'staff' for internal users, 'customer' for external users */
reporterType?: 'staff' | 'customer';
/** Unique identifier for the reporter (defaults to 'error-page') */
reporterId?: string;
/** Reporter's email address (defaults to 'unknown@example.com') */
reporterEmail?: string;
/** Reporter's display name (defaults to 'Error Page Reporter') */
reporterName?: string;
/** Convex API reference for bug reports */
bugReportApi: {
create: FunctionReference<'mutation'>;
};
/** Button variant (default: 'outline') */
variant?: 'filled' | 'outline' | 'light' | 'subtle';
/** Button color (default: 'red') */
color?: string;
/** Callback when submission succeeds */
onSuccess?: () => void;
/** Callback when submission fails */
onError?: (error: Error) => void;
}
interface BrowserInfo {
userAgent: string;
platform: string;
language: string;
cookiesEnabled: boolean;
onLine: boolean;
screenWidth: number;
screenHeight: number;
colorDepth: number;
pixelRatio: number;
}
/**
* A self-contained bug report button designed for error pages.
* Pre-fills the error details and allows users to submit bug reports
* directly from error pages.
*
* @example
* ```tsx
* import { ErrorBugReportButton } from '@fatagnus/convex-feedback/react';
* import { api } from './convex/_generated/api';
*
* function ErrorPage({ error }: { error: Error }) {
* return (
*
*
Something went wrong
*
*
* );
* }
* ```
*/
export function ErrorBugReportButton({
error,
reporterType = 'staff',
reporterId = 'error-page',
reporterEmail = 'unknown@example.com',
reporterName = 'Error Page Reporter',
bugReportApi,
variant = 'outline',
color = 'red',
onSuccess,
onError,
}: ErrorBugReportButtonProps) {
const [opened, { open, close }] = useDisclosure(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const createReport = useMutation(bugReportApi.create);
const form = useForm({
initialValues: {
title: `Error: ${error?.message?.slice(0, 50) || 'Unknown error'}${(error?.message?.length || 0) > 50 ? '...' : ''}`,
description: '',
severity: 'high' as 'low' | 'medium' | 'high' | 'critical',
},
validate: {
title: (value) =>
value.trim().length < 5 ? 'Title must be at least 5 characters' : null,
description: (value) =>
value.trim().length < 10
? 'Please provide more details (at least 10 characters)'
: null,
},
});
const getBrowserInfo = useCallback((): BrowserInfo => {
return {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
cookiesEnabled: navigator.cookieEnabled,
onLine: navigator.onLine,
screenWidth: window.screen.width,
screenHeight: window.screen.height,
colorDepth: window.screen.colorDepth,
pixelRatio: window.devicePixelRatio,
};
}, []);
const getErrorDetails = useCallback((): string => {
const parts = [
'--- Error Details ---',
`Message: ${error?.message || 'Unknown error'}`,
];
if (error?.stack) {
parts.push('', 'Stack Trace:', error.stack);
}
return parts.join('\n');
}, [error]);
const handleSubmit = async (values: typeof form.values) => {
setIsSubmitting(true);
try {
// Build the full description with error details
const fullDescription = [
values.description,
'',
getErrorDetails(),
].join('\n');
// Create the report
await createReport({
title: values.title,
description: fullDescription,
severity: values.severity,
reporterType,
reporterId,
reporterEmail,
reporterName,
url: window.location.href,
route: window.location.pathname,
browserInfo: JSON.stringify(getBrowserInfo()),
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
networkState: navigator.onLine ? 'online' : 'offline',
});
notifications.show({
title: 'Bug report submitted',
message: 'Thank you for reporting this error! We will investigate.',
color: 'green',
icon: ,
});
onSuccess?.();
form.reset();
close();
} catch (submitError) {
console.error('Failed to submit bug report:', submitError);
notifications.show({
title: 'Submission failed',
message: 'Could not submit bug report. Please try again.',
color: 'red',
});
onError?.(submitError instanceof Error ? submitError : new Error(String(submitError)));
} finally {
setIsSubmitting(false);
}
};
const handleOpen = () => {
// Reset form with current error info
form.setValues({
title: `Error: ${error?.message?.slice(0, 50) || 'Unknown error'}${(error?.message?.length || 0) > 50 ? '...' : ''}`,
description: '',
severity: 'high',
});
open();
};
return (
<>
}
onClick={handleOpen}
variant={variant}
color={color}
>
Report Bug
Report This Error
}
size="md"
>
>
);
}
export default ErrorBugReportButton;