'use client' import { useSupabaseClient } from '@supabase/auth-helpers-react' import { Check, MessageSquareQuote, X } from 'lucide-react' import { usePathname } from 'next/navigation' import { type CSSProperties, type MouseEventHandler, forwardRef, useReducer, useRef, useState, } from 'react' import { type Database, useIsLoggedIn } from 'common' import { Button, cn } from 'ui' import { useSendFeedbackMutation } from '~/lib/fetch/feedback' import { useSendTelemetryEvent } from '~/lib/telemetry' import { FeedbackModal, type FeedbackFields } from './FeedbackModal' import { getNotionTeam, getSanitizedTabParams } from './Feedback.utils' const FeedbackButton = forwardRef< HTMLButtonElement, { isYes: boolean; onClick: MouseEventHandler; visible: boolean } >(({ isYes, onClick, visible }, ref) => { const isLoggedIn = useIsLoggedIn() if (!isLoggedIn) return null return ( ) }) FeedbackButton.displayName = 'FeedbackButton' type Response = 'yes' | 'no' enum StateType { Unanswered = 'unanswered', Followup = 'followup', } type State = { type: StateType.Unanswered } | { type: StateType.Followup; response: Response } type Action = { event: 'VOTED'; response: Response } const initialState = { type: StateType.Unanswered } satisfies State function reducer(state: State, action: Action) { switch (action.event) { case 'VOTED': if (state.type === StateType.Unanswered) return { type: StateType.Followup, response: action.response } default: return state } } function Feedback({ className }: { className?: string }) { const [state, dispatch] = useReducer(reducer, initialState) const [modalOpen, setModalOpen] = useState(false) const feedbackButtonRef = useRef(null) const pathname = usePathname() ?? '' const sendTelemetryEvent = useSendTelemetryEvent() const { mutate: sendFeedbackComment } = useSendFeedbackMutation() const supabase = useSupabaseClient() const unanswered = state.type === 'unanswered' const isYes = 'response' in state && state.response === 'yes' const isNo = 'response' in state && state.response === 'no' const showYes = unanswered || isYes const showNo = unanswered || isNo async function sendFeedbackVote(response: Response) { const { error } = await supabase.from('feedback').insert({ vote: response, page: pathname, metadata: { query: getSanitizedTabParams(), }, }) if (error) console.error(error) } function handleVote(response: Response) { sendTelemetryEvent({ category: 'docs', action: 'feedback_voted', label: response, }) sendFeedbackVote(response) dispatch({ event: 'VOTED', response }) // Focus so screen reader users are aware of the new element setTimeout(() => { feedbackButtonRef.current?.focus() // Wait for element to show up first }, 700) } function refocusButton() { setTimeout(() => { feedbackButtonRef.current?.focus() // Wait for modal to disappear and button to become focusable again }, 100) } async function handleSubmit({ page, comment, title }: FeedbackFields) { sendFeedbackComment({ message: comment, pathname: page, title, // @ts-expect-error -- can't click this button without having a state.response isHelpful: state.response === 'yes', team: getNotionTeam(pathname), }) setModalOpen(false) refocusButton() } return (

Is this helpful?

Thanks for your feedback! setModalOpen(true)} isYes={isYes} visible={!unanswered} />
{ setModalOpen(false) refocusButton() }} onSubmit={handleSubmit} />
) } export { Feedback }