editor-survey.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import OLButton from '@/shared/components/ol/ol-button'
  2. import OLForm from '@/shared/components/ol/ol-form'
  3. import OLFormCheckbox from '@/shared/components/ol/ol-form-checkbox'
  4. import OLFormGroup from '@/shared/components/ol/ol-form-group'
  5. import OLIconButton from '@/shared/components/ol/ol-icon-button'
  6. import { OLToast } from '@/shared/components/ol/ol-toast'
  7. import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
  8. import useTutorial from '@/shared/hooks/promotions/use-tutorial'
  9. import { memo, useCallback, useEffect, useMemo, useState } from 'react'
  10. import { sendMB } from '@/infrastructure/event-tracking'
  11. import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
  12. import { useTranslation } from 'react-i18next'
  13. type EditorSurveyPage = 'ease-of-use' | 'meets-my-needs' | 'thank-you'
  14. export default memo(function EditorSurvey() {
  15. return (
  16. <OLToastContainer className="editor-survey-toast">
  17. <EditorSurveyContent />
  18. </OLToastContainer>
  19. )
  20. })
  21. const TUTORIAL_KEY = 'editor-popup-ux-survey-03-2026'
  22. const EditorSurveyContent = () => {
  23. const [easeOfUse, setEaseOfUse] = useState<number | null>(null)
  24. const [meetsMyNeeds, setMeetsMyNeeds] = useState<number | null>(null)
  25. const [page, setPage] = useState<EditorSurveyPage>('ease-of-use')
  26. const newEditor = useIsNewEditorEnabled()
  27. const { t } = useTranslation()
  28. const {
  29. tryShowingPopup: tryShowingSurvey,
  30. showPopup: showSurvey,
  31. dismissTutorial: dismissSurvey,
  32. completeTutorial: completeSurvey,
  33. checkCompletion: checkSurveyCompletion,
  34. } = useTutorial(TUTORIAL_KEY, {
  35. name: TUTORIAL_KEY,
  36. })
  37. useEffect(() => {
  38. if (!checkSurveyCompletion()) {
  39. tryShowingSurvey()
  40. }
  41. }, [checkSurveyCompletion, tryShowingSurvey])
  42. const onSubmit = useCallback(() => {
  43. sendMB('editor-survey-submit', {
  44. easeOfUse,
  45. meetsMyNeeds,
  46. newEditor,
  47. })
  48. setPage('thank-you')
  49. completeSurvey({ event: 'promo-click', action: 'complete' })
  50. }, [easeOfUse, meetsMyNeeds, completeSurvey, newEditor])
  51. if (!showSurvey && page !== 'thank-you') {
  52. return null
  53. }
  54. if (page === 'ease-of-use') {
  55. return (
  56. <OLToast
  57. className="editor-survey-question-toast"
  58. content={
  59. <EditorSurveyQuestion
  60. questionText={t('overleaf_is_easy_to_use')}
  61. name="ease-of-use"
  62. buttonText={t('next')}
  63. onButtonClick={() => setPage('meets-my-needs')}
  64. value={easeOfUse}
  65. onValueChange={setEaseOfUse}
  66. onDismiss={dismissSurvey}
  67. />
  68. }
  69. type="info"
  70. />
  71. )
  72. }
  73. if (page === 'meets-my-needs') {
  74. return (
  75. <OLToast
  76. className="editor-survey-question-toast"
  77. content={
  78. <EditorSurveyQuestion
  79. questionText={t('overleafs_functionality_meets_my_needs')}
  80. name="meets-my-needs"
  81. buttonText={t('submit_title')}
  82. onButtonClick={onSubmit}
  83. value={meetsMyNeeds}
  84. onValueChange={setMeetsMyNeeds}
  85. onDismiss={dismissSurvey}
  86. />
  87. }
  88. type="info"
  89. />
  90. )
  91. }
  92. return <OLToast type="info" content={t('thank_you')} autoHide delay={3000} />
  93. }
  94. const EditorSurveyQuestion = ({
  95. onDismiss,
  96. name,
  97. questionText,
  98. buttonText,
  99. onButtonClick,
  100. value,
  101. onValueChange,
  102. }: {
  103. onDismiss: () => void
  104. name: string
  105. questionText: string
  106. buttonText: string
  107. onButtonClick: () => void
  108. value: number | null
  109. onValueChange: (newValue: number) => void
  110. }) => {
  111. const { t } = useTranslation()
  112. const options = useMemo(
  113. () => [
  114. {
  115. value: 1,
  116. description: t('strongly_disagree'),
  117. },
  118. {
  119. value: 2,
  120. description: t('disagree'),
  121. },
  122. {
  123. value: 3,
  124. description: t('neither_agree_nor_disagree'),
  125. },
  126. {
  127. value: 4,
  128. description: t('agree'),
  129. },
  130. {
  131. value: 5,
  132. description: t('strongly_agree'),
  133. },
  134. ],
  135. [t]
  136. )
  137. const onChange = useCallback(
  138. (event: React.ChangeEvent<HTMLInputElement>) => {
  139. const newValue = event.target.value
  140. if (newValue) {
  141. onValueChange(Number(newValue))
  142. }
  143. },
  144. [onValueChange]
  145. )
  146. return (
  147. <div className="editor-survey-question">
  148. <div className="editor-survey-question-top-line">
  149. <div>{t('your_feedback_matters_answer_two_quick_questions')}</div>
  150. <OLIconButton
  151. variant="ghost"
  152. size="sm"
  153. icon="close"
  154. accessibilityLabel={t('close')}
  155. onClick={onDismiss}
  156. />
  157. </div>
  158. <div>
  159. <label className="editor-survey-question-label" htmlFor={name}>
  160. {questionText}
  161. </label>
  162. <OLForm className="editor-survey-question-form">
  163. <OLFormGroup className="editor-survey-question-options">
  164. {options.map(({ value: optionValue, description }) => (
  165. <OLFormCheckbox
  166. key={optionValue}
  167. name={name}
  168. type="radio"
  169. value={optionValue}
  170. id={optionValue.toString()}
  171. label={optionValue}
  172. checked={optionValue === value}
  173. onChange={onChange}
  174. aria-label={`${optionValue} ${description}`}
  175. />
  176. ))}
  177. </OLFormGroup>
  178. </OLForm>
  179. <div className="editor-survey-option-labels">
  180. <div>{t('strongly_disagree')}</div>
  181. <div>{t('strongly_agree')}</div>
  182. </div>
  183. </div>
  184. <OLButton
  185. size="sm"
  186. variant="secondary"
  187. onClick={onButtonClick}
  188. disabled={!value}
  189. >
  190. {buttonText}
  191. </OLButton>
  192. </div>
  193. )
  194. }