editor-survey.tsx 5.5 KB

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