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 ( <> Report This Error } size="md" >