From aa2b1662663496fcf9e170498dbde0e2323bda64 Mon Sep 17 00:00:00 2001 From: "daily.dev Bot" Date: Thu, 29 Jan 2026 08:44:29 +0000 Subject: [PATCH 1/4] feat: add FeedbackWidget for user feedback submission (ENG-499) Add a floating feedback button that opens a drawer where users can submit feedback categorized by type (bug, feature request, general). Changes: - Add FeedbackWidget component with category selection and text input - Add GraphQL mutation and types for feedback submission - Integrate FeedbackWidget into MainLayout for authenticated users Features: - Floating button in bottom-right corner - Drawer on desktop, full-height drawer on mobile - Category selection with chips - Character counter for description - Toast notifications for success/error - Rate limiting handled by backend Co-Authored-By: Claude Opus 4.5 --- packages/shared/src/components/MainLayout.tsx | 2 + .../components/feedback/FeedbackWidget.tsx | 186 ++++++++++++++++++ .../shared/src/components/feedback/index.ts | 1 + packages/shared/src/graphql/feedback.ts | 39 ++++ 4 files changed, 228 insertions(+) create mode 100644 packages/shared/src/components/feedback/FeedbackWidget.tsx create mode 100644 packages/shared/src/components/feedback/index.ts create mode 100644 packages/shared/src/graphql/feedback.ts diff --git a/packages/shared/src/components/MainLayout.tsx b/packages/shared/src/components/MainLayout.tsx index 35045ae57b..7c130bee0e 100644 --- a/packages/shared/src/components/MainLayout.tsx +++ b/packages/shared/src/components/MainLayout.tsx @@ -33,6 +33,7 @@ import { AuthTriggers } from '../lib/auth'; import PlusMobileEntryBanner from './banners/PlusMobileEntryBanner'; import usePlusEntry from '../hooks/usePlusEntry'; import { SearchProvider } from '../contexts/search/SearchContext'; +import { FeedbackWidget } from './feedback'; const GoBackHeaderMobile = dynamic( () => @@ -215,6 +216,7 @@ function MainLayoutComponent({ )} {children} + ); } diff --git a/packages/shared/src/components/feedback/FeedbackWidget.tsx b/packages/shared/src/components/feedback/FeedbackWidget.tsx new file mode 100644 index 0000000000..78051738c7 --- /dev/null +++ b/packages/shared/src/components/feedback/FeedbackWidget.tsx @@ -0,0 +1,186 @@ +import type { ReactElement } from 'react'; +import React, { useState, useCallback } from 'react'; +import classNames from 'classnames'; +import { useMutation } from '@tanstack/react-query'; +import { Button, ButtonVariant, ButtonSize } from '../buttons/Button'; +import { FeedbackIcon } from '../icons'; +import { Drawer, DrawerPosition } from '../drawers/Drawer'; +import Textarea from '../fields/Textarea'; +import { useToastNotification } from '../../hooks/useToastNotification'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useViewSize, ViewSize } from '../../hooks/useViewSize'; +import { + FeedbackCategory, + submitFeedback, + type FeedbackInput, +} from '../../graphql/feedback'; +import { + Typography, + TypographyType, + TypographyColor, +} from '../typography/Typography'; + +const FEEDBACK_MAX_LENGTH = 2000; + +const categoryOptions: { value: FeedbackCategory; label: string }[] = [ + { value: FeedbackCategory.Bug, label: 'Bug Report' }, + { value: FeedbackCategory.FeatureRequest, label: 'Feature Request' }, + { value: FeedbackCategory.General, label: 'General Feedback' }, + { value: FeedbackCategory.Other, label: 'Other' }, +]; + +interface FeedbackWidgetProps { + className?: string; +} + +export function FeedbackWidget({ + className, +}: FeedbackWidgetProps): ReactElement | null { + const { user } = useAuthContext(); + const { displayToast } = useToastNotification(); + const isMobile = useViewSize(ViewSize.MobileL); + + const [isOpen, setIsOpen] = useState(false); + const [category, setCategory] = useState( + FeedbackCategory.General, + ); + const [description, setDescription] = useState(''); + + const { mutate: submitMutation, isPending } = useMutation({ + mutationFn: (input: FeedbackInput) => submitFeedback(input), + onSuccess: () => { + displayToast('Thank you for your feedback!'); + setIsOpen(false); + setDescription(''); + setCategory(FeedbackCategory.General); + }, + onError: () => { + displayToast('Failed to submit feedback. Please try again.'); + }, + }); + + const handleSubmit = useCallback(() => { + if (!description.trim()) { + displayToast('Please enter your feedback'); + return; + } + + submitMutation({ + category, + description: description.trim(), + pageUrl: typeof window !== 'undefined' ? window.location.href : undefined, + userAgent: + typeof navigator !== 'undefined' ? navigator.userAgent : undefined, + }); + }, [category, description, submitMutation, displayToast]); + + const handleClose = useCallback(() => { + if (!isPending) { + setIsOpen(false); + } + }, [isPending]); + + // Only show for authenticated users + if (!user) { + return null; + } + + return ( + <> + {/* Floating button */} + + + {/* Feedback form drawer */} + +
+ + Help us improve by sharing your thoughts + + + {/* Category selector */} +
+ + Category + +
+ {categoryOptions.map((option) => ( + + ))} +
+
+ + {/* Description textarea */} +