import { AnimatePresence, motion } from 'framer-motion' 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 { useDebounce } from 'use-debounce' import { useParams } from 'common' import { InlineLink } from 'components/ui/InlineLink' import { useFeedbackCategoryQuery } from 'data/feedback/feedback-category' import { useSendFeedbackMutation } from 'data/feedback/feedback-send' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { timeout } from 'lib/helpers' import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, PopoverSeparator_Shadcn_, TextArea_Shadcn_, } from 'ui' import { Admonition } from 'ui-patterns' import { convertB64toBlob, uploadAttachment } from './FeedbackDropdown.utils' interface FeedbackWidgetProps { feedback: string screenshot: string | undefined onClose: () => void setFeedback: (value: string) => void setScreenshot: (value: string | undefined) => void } export 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 { data: org } = useSelectedOrganizationQuery() const uploadButtonRef = useRef(null) const [isSending, setSending] = useState(false) const [isSavingScreenshot, setIsSavingScreenshot] = useState(false) const [isFeedbackSent, setIsFeedbackSent] = useState(false) const [debouncedFeedback] = useDebounce(feedback, 450) const { data: category } = useFeedbackCategoryQuery({ prompt: debouncedFeedback, }) 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" />
{category === 'support' && (

Please{' '} open a support ticket {' '} to get help with this issue, as we do not reply to all product feedback.

)}

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" >
) : ( { if (uploadButtonRef.current) (uploadButtonRef.current as any).click() }} > Upload screenshot captureScreenshot()} > Capture screenshot )}
) } 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

) }