import { toPng } from 'html-to-image' import { Camera, CircleCheck, Image as ImageIcon, Upload, X } from 'lucide-react' import Link from 'next/link' import { useRouter } from 'next/router' import { ChangeEvent, useEffect, useRef, useState } from 'react' import { toast } from 'sonner' import { PopoverSeparator } from '@ui/components/shadcn/ui/popover' import { useParams } from 'common' import { useSendFeedbackMutation } from 'data/feedback/feedback-send' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization' import { timeout } from 'lib/helpers' import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, TextArea_Shadcn_, } from 'ui' import { convertB64toBlob, uploadAttachment } from './FeedbackDropdown.utils' interface FeedbackWidgetProps { feedback: string screenshot: string | undefined onClose: () => void setFeedback: (value: string) => void setScreenshot: (value: string | undefined) => void } const FeedbackWidget = ({ feedback, screenshot, onClose, setFeedback, setScreenshot, }: FeedbackWidgetProps) => { const FEEDBACK_STORAGE_KEY = 'feedback_content' const SCREENSHOT_STORAGE_KEY = 'screenshot' const router = useRouter() const { ref, slug } = useParams() const org = useSelectedOrganization() const uploadButtonRef = useRef(null) const [isSending, setSending] = useState(false) const [isSavingScreenshot, setIsSavingScreenshot] = useState(false) const [isFeedbackSent, setIsFeedbackSent] = useState(false) const { mutate: sendEvent } = useSendEventMutation() const { mutate: submitFeedback } = useSendFeedbackMutation({ onSuccess: () => { setIsFeedbackSent(true) setFeedback('') setScreenshot(undefined) localStorage.removeItem(FEEDBACK_STORAGE_KEY) localStorage.removeItem(SCREENSHOT_STORAGE_KEY) setSending(false) }, onError: (error) => { toast.error(`Failed to submit feedback: ${error.message}`) setSending(false) }, }) useEffect(() => { const storedFeedback = localStorage.getItem(FEEDBACK_STORAGE_KEY) if (storedFeedback) { setFeedback(storedFeedback) } const storedScreenshot = localStorage.getItem(SCREENSHOT_STORAGE_KEY) if (storedScreenshot) { setScreenshot(storedScreenshot) } }, []) useEffect(() => { localStorage.setItem(FEEDBACK_STORAGE_KEY, feedback) }, [feedback]) useEffect(() => { if (screenshot) { localStorage.setItem(SCREENSHOT_STORAGE_KEY, screenshot) } }, [screenshot]) const captureScreenshot = async () => { setIsSavingScreenshot(true) function filter(node: HTMLElement) { if ((node?.children ?? []).length > 0) { return node.children[0].id !== 'feedback-widget' } return true } // Give time for dropdown to close await timeout(100) toPng(document.body, { filter }) .then((dataUrl: any) => { localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) setScreenshot(dataUrl) }) .catch(() => toast.error('Failed to capture screenshot')) .finally(() => { setIsSavingScreenshot(false) }) } const onFilesUpload = async (event: ChangeEvent) => { event.persist() const [file] = event.target.files || (event as any).dataTransfer.items const reader = new FileReader() reader.onload = function (event) { const dataUrl = event.target?.result if (typeof dataUrl === 'string') { setScreenshot(dataUrl) localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) } } reader.readAsDataURL(file) event.target.value = '' } const handlePasteEvent = async () => { // [Joshen] Support pasting images via Cmd / Ctrl + V const [data] = await navigator.clipboard.read() if (screenshot === undefined && data.types[0] === 'image/png') { const blob = await data.getType('image/png') const reader = new FileReader() reader.onload = function (event) { const dataUrl = event.target?.result if (typeof dataUrl === 'string') { setScreenshot(dataUrl) localStorage.setItem(SCREENSHOT_STORAGE_KEY, dataUrl) } } reader.readAsDataURL(blob) } } const sendFeedback = async () => { if (feedback.length === 0 && screenshot !== undefined) { return toast.error('Please include a message in your feedback.') } else if (feedback.length > 0) { setSending(true) const attachmentUrl = screenshot ? await uploadAttachment(ref as string, screenshot) : undefined const formattedFeedback = attachmentUrl !== undefined ? `${feedback}\n\nAttachments:\n${attachmentUrl}` : feedback submitFeedback({ projectRef: ref, organizationSlug: slug, message: formattedFeedback, pathname: router.asPath, }) } } return isFeedbackSent ? ( ) : ( <> setFeedback(e.target.value)} onPaste={handlePasteEvent} className="text-sm mt-4 mb-1" /> Have a technical issue? Contact{' '} support {' '} or{' '} see docs . {screenshot !== undefined ? ( { const blob = convertB64toBlob(screenshot) const blobUrl = URL.createObjectURL(blob) window.open(blobUrl, '_blank') }} className="cursor-pointer rounded h-[26px] w-[30px] border border-control relative bg-cover bg-center bg-no-repeat" > { event.stopPropagation() setScreenshot(undefined) }} > ) : ( { if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} > Upload screenshot captureScreenshot()} > Capture screenshot )} { sendFeedback() sendEvent({ action: 'send_feedback_button_clicked', groups: { project: ref, organization: org?.slug }, }) }} > Send > ) } export default FeedbackWidget const ThanksMessage = ({ onClose }: { onClose: () => void }) => { return ( Your feedback has been sent. Thanks! We do not always respond to feedback. If you require assistance, please contact support instead. Create a Support Ticket Close ) }
Have a technical issue?
Contact{' '} support {' '} or{' '} see docs .
Your feedback has been sent. Thanks!
We do not always respond to feedback. If you require assistance, please contact support instead.
Create a Support Ticket