Skip to content

Commit d250c38

Browse files
authored
add feedback widget with posthog (#429)
1 parent 1b1b93d commit d250c38

File tree

4 files changed

+414
-0
lines changed

4 files changed

+414
-0
lines changed

astro.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default defineConfig({
5757
PageSidebar: './src/components/PageSidebarWithBadges.astro',
5858
LanguageSelect: './src/components/LanguageSelectWithGetStarted.astro',
5959
Banner: './src/components/BannerWithPersistentAnnouncement.astro',
60+
Footer: './src/components/FooterWithFeedback.astro',
6061
},
6162
expressiveCode: {
6263
themes: ['one-light', 'one-dark-pro'],

src/components/DocsFeedback.tsx

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import React, { useState } from 'react';
2+
3+
// PostHog Survey ID
4+
const POSTHOG_SURVEY_ID = '019c4ba3-c53b-0000-7cef-4903439f7e52';
5+
6+
// Declare PostHog on window
7+
declare global {
8+
interface Window {
9+
posthog?: {
10+
capture: (event: string, properties?: Record<string, unknown>) => void;
11+
};
12+
}
13+
}
14+
15+
const POSITIVE_OPTIONS = [
16+
'The guide worked as expected',
17+
'Easy to find the information I needed',
18+
'Easy to understand',
19+
'Helped me decide to use the product',
20+
'Something else',
21+
];
22+
23+
const NEGATIVE_OPTIONS = [
24+
'Hard to understand',
25+
'Incorrect information or code sample',
26+
'Missing the information I needed',
27+
'Other',
28+
];
29+
30+
// Submit feedback to PostHog
31+
// PostHog surveys use $survey_response, $survey_response_1, $survey_response_2, etc.
32+
// for multiple question responses
33+
const submitFeedbackToPostHog = (
34+
isHelpful: 'yes' | 'no',
35+
reason: string,
36+
additionalFeedback: string,
37+
pageUrl: string
38+
): void => {
39+
if (typeof window === 'undefined' || !window.posthog) {
40+
console.error('PostHog not available');
41+
return;
42+
}
43+
44+
const properties: Record<string, unknown> = {
45+
$survey_id: POSTHOG_SURVEY_ID,
46+
$survey_response: isHelpful === 'yes' ? 'Yes 👍' : 'No 👎',
47+
$survey_response_1: reason, // Selected option (what went wrong / what worked)
48+
pageUrl: pageUrl,
49+
};
50+
51+
if (additionalFeedback.trim()) {
52+
properties.$survey_response_2 = additionalFeedback; // Additional comments
53+
}
54+
55+
window.posthog.capture('survey sent', properties);
56+
};
57+
58+
export const DocsFeedback: React.FC = () => {
59+
const [feedbackState, setFeedbackState] = useState<
60+
'initial' | 'positive' | 'negative' | 'submitted'
61+
>('initial');
62+
const [selectedReason, setSelectedReason] = useState<string>('');
63+
const [additionalFeedback, setAdditionalFeedback] = useState('');
64+
const [isSubmitting, setIsSubmitting] = useState(false);
65+
66+
const handleThumbsUp = () => {
67+
setFeedbackState('positive');
68+
setSelectedReason('');
69+
setAdditionalFeedback('');
70+
};
71+
72+
const handleThumbsDown = () => {
73+
setFeedbackState('negative');
74+
setSelectedReason('');
75+
setAdditionalFeedback('');
76+
};
77+
78+
const handleSubmit = () => {
79+
if (!selectedReason) return;
80+
81+
setIsSubmitting(true);
82+
const pageUrl = typeof window !== 'undefined' ? window.location.pathname : '';
83+
const isPositive = feedbackState === 'positive';
84+
85+
submitFeedbackToPostHog(
86+
isPositive ? 'yes' : 'no',
87+
selectedReason,
88+
additionalFeedback,
89+
pageUrl
90+
);
91+
92+
setIsSubmitting(false);
93+
setFeedbackState('submitted');
94+
};
95+
96+
const handleCancel = () => {
97+
setFeedbackState('initial');
98+
setSelectedReason('');
99+
setAdditionalFeedback('');
100+
};
101+
102+
const options = feedbackState === 'positive' ? POSITIVE_OPTIONS : NEGATIVE_OPTIONS;
103+
104+
if (feedbackState === 'submitted') {
105+
return (
106+
<div className="docs-feedback">
107+
<div className="docs-feedback-success">
108+
<svg
109+
className="docs-feedback-success-icon"
110+
viewBox="0 0 24 24"
111+
fill="none"
112+
stroke="currentColor"
113+
strokeWidth="2"
114+
>
115+
<circle cx="12" cy="12" r="10" />
116+
<path d="M8 12l2.5 2.5L16 9" />
117+
</svg>
118+
<span>Thanks for your feedback!</span>
119+
</div>
120+
</div>
121+
);
122+
}
123+
124+
if (feedbackState === 'positive' || feedbackState === 'negative') {
125+
return (
126+
<div className="docs-feedback">
127+
<div className="docs-feedback-expanded">
128+
<div className="docs-feedback-question">
129+
{feedbackState === 'positive' ? 'Great! What worked best for you?' : 'What went wrong?'}
130+
</div>
131+
<div className="docs-feedback-options">
132+
{options.map((option) => (
133+
<label key={option} className="docs-feedback-option">
134+
<input
135+
type="radio"
136+
name="feedback-reason"
137+
value={option}
138+
checked={selectedReason === option}
139+
onChange={(e) => setSelectedReason(e.target.value)}
140+
/>
141+
<span className="docs-feedback-option-text">{option}</span>
142+
</label>
143+
))}
144+
</div>
145+
<textarea
146+
value={additionalFeedback}
147+
onChange={(e) => setAdditionalFeedback(e.target.value)}
148+
placeholder="Any additional feedback? (optional)"
149+
className="docs-feedback-textarea"
150+
rows={3}
151+
/>
152+
<div className="docs-feedback-actions">
153+
<button
154+
onClick={handleCancel}
155+
className="docs-feedback-cancel-btn"
156+
type="button"
157+
>
158+
Cancel
159+
</button>
160+
<button
161+
onClick={handleSubmit}
162+
disabled={isSubmitting || !selectedReason}
163+
className="docs-feedback-submit-btn"
164+
type="button"
165+
>
166+
{isSubmitting ? 'Submitting...' : 'Submit feedback'}
167+
</button>
168+
</div>
169+
</div>
170+
</div>
171+
);
172+
}
173+
174+
return (
175+
<div className="docs-feedback">
176+
<div className="docs-feedback-initial">
177+
<span className="docs-feedback-label">Was this page helpful?</span>
178+
<div className="docs-feedback-buttons">
179+
<button
180+
onClick={handleThumbsUp}
181+
className="docs-feedback-btn"
182+
aria-label="Yes, this was helpful"
183+
type="button"
184+
>
185+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
186+
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
187+
</svg>
188+
Yes
189+
</button>
190+
<button
191+
onClick={handleThumbsDown}
192+
className="docs-feedback-btn"
193+
aria-label="No, this was not helpful"
194+
type="button"
195+
>
196+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
197+
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
198+
</svg>
199+
No
200+
</button>
201+
</div>
202+
</div>
203+
</div>
204+
);
205+
};
206+
207+
export default DocsFeedback;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
import Default from '@astrojs/starlight/components/Footer.astro';
3+
import DocsFeedback from './DocsFeedback';
4+
---
5+
6+
<div class="docs-feedback-wrapper">
7+
<DocsFeedback client:load />
8+
</div>
9+
10+
<Default />
11+
12+
<style>
13+
.docs-feedback-wrapper {
14+
padding: 1.5rem 0;
15+
margin-bottom: 1rem;
16+
}
17+
</style>

0 commit comments

Comments
 (0)