From 6011c626dffccd0551cd075f53b8446dba0dc157 Mon Sep 17 00:00:00 2001 From: Dikran Samarjian Date: Thu, 21 May 2026 14:02:53 -0700 Subject: [PATCH] csat v1 --- src/components/CsatWidget/index.tsx | 256 ++++++++++++++++++++ src/components/CsatWidget/styles.module.css | 194 +++++++++++++++ src/theme/DocsRoot/index.tsx | 19 ++ static/events.js | 28 ++- 4 files changed, 488 insertions(+), 9 deletions(-) create mode 100644 src/components/CsatWidget/index.tsx create mode 100644 src/components/CsatWidget/styles.module.css create mode 100644 src/theme/DocsRoot/index.tsx diff --git a/src/components/CsatWidget/index.tsx b/src/components/CsatWidget/index.tsx new file mode 100644 index 00000000..bc825995 --- /dev/null +++ b/src/components/CsatWidget/index.tsx @@ -0,0 +1,256 @@ +import clsx from 'clsx'; +import { useLocation } from '@docusaurus/router'; +import React, { useEffect, useMemo, useState } from 'react'; + +import styles from './styles.module.css'; + +const CSAT_STORAGE_PREFIX = 'devvit_docs_csat_submitted:'; + +declare global { + interface Window { + sendV2Event?: (event: unknown) => void; + } +} + +function getStorageKey(pathname: string): string { + return `${CSAT_STORAGE_PREFIX}${pathname}`; +} + +function getReferrerInfo(): { url?: string; domain?: string } { + if (!document.referrer) { + return {}; + } + + try { + const referrerUrl = new URL(document.referrer); + return { + url: document.referrer, + domain: referrerUrl.host, + }; + } catch { + return { + url: document.referrer, + }; + } +} + +type ScoreOption = { + label: string; + value: number; + emoji: string; + reaction: 'positive' | 'negative'; +}; + +const SCORE_OPTIONS: ScoreOption[] = [ + { + label: 'Thumbs down', + value: 1, + emoji: '👎', + reaction: 'negative', + }, + { + label: 'Thumbs up', + value: 5, + emoji: '👍', + reaction: 'positive', + }, +]; + +const NEGATIVE_REASON_OPTIONS = [ + 'The page is unclear', + 'Important details are missing', + 'The instructions did not work', +]; + +export default function CsatWidget(): React.ReactElement | null { + const { pathname } = useLocation(); + const [selectedScore, setSelectedScore] = useState(null); + const [feedback, setFeedback] = useState(''); + const [selectedReasons, setSelectedReasons] = useState([]); + const [isVisible, setIsVisible] = useState(false); + const [isSubmitted, setIsSubmitted] = useState(false); + + const storageKey = useMemo(() => getStorageKey(pathname), [pathname]); + const shouldShowForPath = useMemo(() => { + const normalizedPath = pathname.replace(/\/+$/, '') || '/'; + return normalizedPath !== '/' && normalizedPath !== '/docs'; + }, [pathname]); + + const sendEvent = (action: string, details: Record) => { + if (typeof window === 'undefined' || typeof window.sendV2Event !== 'function') { + return; + } + + window.sendV2Event({ + source: 'docs_csat_widget', + action, + noun: 'csat', + action_info: { + page_type: 'dev_portal_docs', + page_path: pathname, + ...details, + }, + request: { + base_url: window.location.href, + }, + referrer: getReferrerInfo(), + }); + }; + + useEffect(() => { + if (typeof window === 'undefined') { + return; + } + + if (!shouldShowForPath) { + setSelectedScore(null); + setFeedback(''); + setSelectedReasons([]); + setIsSubmitted(false); + setIsVisible(false); + return; + } + + const hasSubmitted = window.sessionStorage.getItem(storageKey) === '1'; + setSelectedScore(null); + setFeedback(''); + setSelectedReasons([]); + setIsSubmitted(false); + setIsVisible(!hasSubmitted); + + if (!hasSubmitted) { + sendEvent('view', { widget: 'docs_csat' }); + } + }, [storageKey, shouldShowForPath]); + + const dismiss = () => { + sendEvent('dismiss', { widget: 'docs_csat' }); + setIsVisible(false); + }; + + const submit = (score: number, extraDetails: Record = {}) => { + if (typeof window === 'undefined') { + return; + } + + sendEvent('submit', { + widget: 'docs_csat', + score, + ...extraDetails, + }); + + window.sessionStorage.setItem(storageKey, '1'); + setIsSubmitted(true); + window.setTimeout(() => { + setIsVisible(false); + }, 1200); + }; + + const onScoreSelect = (option: ScoreOption) => { + const score = option.value; + setSelectedScore(score); + if (option.reaction === 'positive') { + setSelectedReasons([]); + setFeedback(''); + } + + sendEvent('select_score', { + widget: 'docs_csat', + score, + reaction: option.reaction, + }); + + if (option.reaction === 'positive') { + submit(score, { + reaction: option.reaction, + feedback: '', + feedback_length: 0, + reasons: [], + }); + } + }; + + const toggleReason = (reason: string) => { + setSelectedReasons((current) => + current.includes(reason) ? current.filter((item) => item !== reason) : [...current, reason] + ); + }; + + const submitNegativeFeedback = () => { + if (selectedScore !== 1) { + return; + } + + const trimmedFeedback = feedback.trim(); + submit(selectedScore, { + reaction: 'negative', + feedback: trimmedFeedback, + feedback_length: trimmedFeedback.length, + reasons: selectedReasons, + }); + }; + + if (!isVisible) { + return null; + } + + return ( +
+
+

How helpful was this page?

+ +
+
+ {SCORE_OPTIONS.map((option) => ( + + ))} +
+ {selectedScore === 1 && !isSubmitted && ( +
+

Additional feedback (optional)

+