Skip to content

Commit 4d75939

Browse files
committed
Add secondary reason screen to FeedbackWidget
Signed-off-by: bgravenorst <byron.gravenorst@consensys.net>
1 parent d0725b9 commit 4d75939

3 files changed

Lines changed: 192 additions & 23 deletions

File tree

src/components/FeedbackWidget/index.tsx

Lines changed: 116 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,69 @@ import React, { useState } from 'react'
22
import styles from './styles.module.css'
33

44
type Rating = 'yes' | 'no' | null
5-
type Phase = 'initial' | 'comment' | 'submitted'
5+
type Phase = 'initial' | 'reason' | 'submitted'
6+
7+
interface FeedbackOption {
8+
value: string
9+
label: string
10+
description: string
11+
}
12+
13+
const YES_OPTIONS: FeedbackOption[] = [
14+
{
15+
value: 'accurate',
16+
label: 'Accurate',
17+
description: 'Accurately describes the product or feature.',
18+
},
19+
{
20+
value: 'solved',
21+
label: 'Solved my problem',
22+
description: 'Helped me resolve an issue.',
23+
},
24+
{
25+
value: 'easy',
26+
label: 'Easy to understand',
27+
description: 'Easy to follow and comprehend.',
28+
},
29+
{
30+
value: 'helpful',
31+
label: 'Helped me decide to use the product',
32+
description: 'Convinced me to adopt the product or feature.',
33+
},
34+
{
35+
value: 'other',
36+
label: 'Another reason',
37+
description: '',
38+
},
39+
]
40+
41+
const NO_OPTIONS: FeedbackOption[] = [
42+
{
43+
value: 'inaccurate',
44+
label: 'Inaccurate',
45+
description: "Doesn't accurately describe the product or feature.",
46+
},
47+
{
48+
value: 'missing-info',
49+
label: "Couldn't find what I was looking for",
50+
description: 'Missing important information.',
51+
},
52+
{
53+
value: 'hard-to-understand',
54+
label: 'Hard to understand',
55+
description: 'Too complicated or unclear.',
56+
},
57+
{
58+
value: 'code-errors',
59+
label: 'Code sample errors',
60+
description: 'One or more code samples are incorrect.',
61+
},
62+
{
63+
value: 'other',
64+
label: 'Another reason',
65+
description: '',
66+
},
67+
]
668

769
function stripHtml(text: string): string {
870
let prev = text
@@ -29,27 +91,35 @@ declare global {
2991
export default function FeedbackWidget(): React.ReactNode {
3092
const [rating, setRating] = useState<Rating>(null)
3193
const [phase, setPhase] = useState<Phase>('initial')
94+
const [selectedOption, setSelectedOption] = useState('')
3295
const [reason, setReason] = useState('')
3396
const [errorMsg, setErrorMsg] = useState('')
3497
const [submitting, setSubmitting] = useState(false)
3598

36-
const reasonIsEmpty = !sanitizeReason(reason)
99+
const options = rating === 'yes' ? YES_OPTIONS : NO_OPTIONS
100+
const isOtherSelected = selectedOption === 'other'
101+
const canSubmit = selectedOption !== '' && (!isOtherSelected || sanitizeReason(reason).length > 0)
37102

38103
const handleRating = (selected: Rating) => {
39104
setRating(selected)
40-
setPhase('comment')
105+
setSelectedOption('')
106+
setReason('')
107+
setPhase('reason')
41108
}
42109

43110
const handleSubmit = async () => {
44-
if (rating === 'no' && reasonIsEmpty) return
111+
if (!canSubmit) return
45112
setSubmitting(true)
46113
setErrorMsg('')
47114

115+
const reasonText = isOtherSelected ? reason.trim() : ''
116+
48117
window.dataLayer?.push({
49118
event: 'docs_feedback',
50119
page_url: window.location.pathname,
51120
rating,
52-
reason: reason.trim(),
121+
option: selectedOption,
122+
reason: reasonText,
53123
})
54124

55125
try {
@@ -59,7 +129,8 @@ export default function FeedbackWidget(): React.ReactNode {
59129
body: JSON.stringify({
60130
page_url: window.location.pathname,
61131
rating,
62-
reason: reason.trim(),
132+
option: selectedOption,
133+
reason: reasonText,
63134
}),
64135
})
65136
if (!res.ok) {
@@ -102,23 +173,45 @@ export default function FeedbackWidget(): React.ReactNode {
102173
</>
103174
)}
104175

105-
{phase === 'comment' && (
176+
{phase === 'reason' && (
106177
<div className={styles.commentSection}>
107-
<label htmlFor="feedback-reason" className={styles.label}>
108-
{rating === 'yes' ? 'What worked well? (optional)' : 'What went wrong?'}
109-
</label>
110-
<textarea
111-
id="feedback-reason"
112-
className={styles.textarea}
113-
value={reason}
114-
onChange={e => setReason(e.target.value)}
115-
placeholder={
116-
rating === 'yes' ? 'Tell us what you liked...' : 'Tell us what we can improve...'
117-
}
118-
maxLength={1000}
119-
rows={3}
120-
required={rating === 'no'}
121-
/>
178+
<p className={styles.label}>
179+
{rating === 'yes' ? 'What worked well?' : 'What went wrong?'}
180+
</p>
181+
<div className={styles.optionList} role="radiogroup">
182+
{options.map(opt => (
183+
<label key={opt.value} className={styles.option}>
184+
<input
185+
type="radio"
186+
name="feedback-option"
187+
value={opt.value}
188+
checked={selectedOption === opt.value}
189+
onChange={() => setSelectedOption(opt.value)}
190+
className={styles.radioInput}
191+
/>
192+
<span className={styles.optionText}>
193+
<span className={styles.optionLabel}>{opt.label}</span>
194+
{opt.description && (
195+
<span className={styles.optionDescription}>{opt.description}</span>
196+
)}
197+
{opt.value === 'other' && isOtherSelected && (
198+
<textarea
199+
className={styles.inlineTextarea}
200+
value={reason}
201+
onChange={e => {
202+
e.stopPropagation()
203+
setReason(e.target.value)
204+
}}
205+
onClick={e => e.stopPropagation()}
206+
placeholder="Tell us more..."
207+
maxLength={1000}
208+
rows={7}
209+
/>
210+
)}
211+
</span>
212+
</label>
213+
))}
214+
</div>
122215
{errorMsg && (
123216
<p className={styles.error} role="alert">
124217
{errorMsg}
@@ -129,7 +222,7 @@ export default function FeedbackWidget(): React.ReactNode {
129222
type="button"
130223
className={styles.submitBtn}
131224
onClick={handleSubmit}
132-
disabled={submitting || (rating === 'no' && reasonIsEmpty)}>
225+
disabled={submitting || !canSubmit}>
133226
{submitting ? 'Sending...' : errorMsg ? 'Try again' : 'Submit'}
134227
</button>
135228
</div>

src/components/FeedbackWidget/styles.module.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,74 @@
138138
color: var(--ifm-color-danger, var(--ifm-color-emphasis-600));
139139
margin: 12px 0 0;
140140
}
141+
142+
.optionList {
143+
display: flex;
144+
flex-direction: column;
145+
gap: 12px;
146+
margin-bottom: 16px;
147+
}
148+
149+
.option {
150+
display: flex;
151+
align-items: flex-start;
152+
gap: 10px;
153+
cursor: pointer;
154+
}
155+
156+
.radioInput {
157+
flex-shrink: 0;
158+
margin-top: 3px;
159+
width: 16px !important;
160+
height: 16px !important;
161+
appearance: auto !important;
162+
accent-color: var(--ifm-color-primary);
163+
cursor: pointer;
164+
border: revert !important;
165+
}
166+
167+
.optionText {
168+
display: flex;
169+
flex-direction: column;
170+
gap: 2px;
171+
}
172+
173+
.optionLabel {
174+
font-size: 1.4rem;
175+
font-weight: 500;
176+
color: var(--ifm-color-content);
177+
line-height: 150%;
178+
}
179+
180+
.optionDescription {
181+
font-size: 1.3rem;
182+
color: var(--ifm-color-content-secondary);
183+
line-height: 150%;
184+
}
185+
186+
.inlineTextarea {
187+
display: block;
188+
box-sizing: border-box;
189+
appearance: none;
190+
width: 100%;
191+
margin-top: 8px;
192+
padding: 10px 12px;
193+
font-size: 1.4rem;
194+
color: var(--ifm-color-content);
195+
background: var(--ifm-background-surface-color) !important;
196+
border: 1px solid var(--ifm-color-emphasis-300) !important;
197+
border-radius: 8px;
198+
font-family: var(--ifm-font-family-base);
199+
resize: vertical;
200+
line-height: 1.6;
201+
outline: none;
202+
}
203+
204+
.inlineTextarea::placeholder {
205+
color: var(--ifm-color-emphasis-500);
206+
}
207+
208+
.inlineTextarea:focus {
209+
outline: none;
210+
border-color: var(--ifm-color-primary) !important;
211+
}

src/globals.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ declare module '*.svg' {
33
const content: FC<SVGProps<SVGElement>>
44
export default content
55
}
6+
7+
declare module '*.module.css' {
8+
const classes: Record<string, string>
9+
export default classes
10+
}

0 commit comments

Comments
 (0)