Skip to content

Commit ea9d3c0

Browse files
samkimclaude
andcommitted
Add interactive feedback widget to documentation pages
Implements a new feedback component in the TOC sidebar that allows users to provide feedback about documentation pages. Also updates the CTA component with improved styling and new demo booking link. Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 42eb2f6 commit ea9d3c0

File tree

4 files changed

+270
-13
lines changed

4 files changed

+270
-13
lines changed

app/layout.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Logo from "@/components/icons/logo.svg";
77
import LogoIcon from "@/components/icons/logo-icon.svg";
88
import BannerContents from "@/components/banner";
99
import Providers from "@/components/providers";
10-
import { TocCTA } from "@/components/cta";
10+
import { TocExtraContent } from "@/components/toc-extra-content";
1111
import Scripts from "@/components/scripts";
1212
import type { Metadata, ResolvingMetadata } from "next";
1313
import { SpeedInsights } from "@vercel/speed-insights/next";
@@ -89,15 +89,10 @@ export default async function RootLayout({ children }) {
8989
}}
9090
pageMap={pageMap}
9191
feedback={{
92-
content: (
93-
<span>
94-
Something unclear?
95-
<br />
96-
Create an issue →
97-
</span>
98-
),
92+
content: null,
9993
}}
100-
toc={{ backToTop: true, extraContent: <TocCTA /> }}
94+
editLink={null}
95+
toc={{ backToTop: true, extraContent: <TocExtraContent /> }}
10196
>
10297
<OurLayout>
10398
<SpeedInsights />

components/cta.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export function TocCTA() {
1010
const isCommercial = pathname?.startsWith("/authzed/");
1111

1212
return isCommercial ? (
13-
<div className="mt-8 border-t pt-8 dark:border-neutral-800 contrast-more:border-neutral-400">
14-
<div className="text-xs mb-2 font-semibold text-gray-500 dark:text-gray-400">
15-
Talk to us
13+
<div className="pt-4 pb-8">
14+
<div className="text-sm mb-4 font-semibold">
15+
Explore your use case
1616
</div>
17-
<Link href="https://authzed.com/call?utm_source=docs" className="cursor-pointer">
17+
<Link href="https://authzed.com/schedule-demo" className="cursor-pointer">
1818
<Button variant="default" size="sm" className="w-full cursor-pointer">
1919
Book a demo
2020
</Button>

components/feedback.tsx

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
"use client";
2+
3+
import { useState, useEffect } from "react";
4+
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5+
import { faThumbsUp, faThumbsDown } from "@fortawesome/free-solid-svg-icons";
6+
import { faGithub } from "@fortawesome/free-brands-svg-icons";
7+
import posthog from "posthog-js";
8+
9+
export function Feedback() {
10+
const [isVisible, setIsVisible] = useState(false);
11+
const [showForm, setShowForm] = useState(false);
12+
const [showThankYou, setShowThankYou] = useState(false);
13+
const [isHelpful, setIsHelpful] = useState<boolean | null>(null);
14+
const [formData, setFormData] = useState({
15+
hadRightInfo: false,
16+
easyToUnderstand: false,
17+
somethingElse: false,
18+
missingInfo: false,
19+
difficultToUnderstand: false,
20+
containsErrors: false,
21+
comments: "",
22+
email: "",
23+
});
24+
25+
useEffect(() => {
26+
const timer = setTimeout(() => {
27+
setIsVisible(true);
28+
}, 3000);
29+
30+
return () => clearTimeout(timer);
31+
}, []);
32+
33+
const handleYes = () => {
34+
posthog.capture("docs-feedback", { helpful: true });
35+
setIsHelpful(true);
36+
setShowForm(true);
37+
};
38+
39+
const handleNo = () => {
40+
posthog.capture("docs-feedback", { helpful: false });
41+
setIsHelpful(false);
42+
setShowForm(true);
43+
};
44+
45+
const handleFormSubmit = (e: React.FormEvent) => {
46+
e.preventDefault();
47+
48+
if (formData.email) {
49+
posthog.identify(formData.email);
50+
}
51+
52+
posthog.capture("docs-feedback", {
53+
helpful: isHelpful,
54+
details: formData,
55+
});
56+
57+
// If negative feedback, create GitHub issue
58+
if (!isHelpful) {
59+
const pageUrl = window.location.href;
60+
const pageTitle = document.title;
61+
62+
// Build issue body
63+
const issues: string[] = [];
64+
if (formData.missingInfo) issues.push("Missing info");
65+
if (formData.difficultToUnderstand) issues.push("Difficult to understand");
66+
if (formData.containsErrors) issues.push("Contains errors");
67+
68+
const issueBody = `**Page:** ${pageUrl}
69+
70+
**What went wrong:**
71+
${issues.map((issue) => `- ${issue}`).join("\n")}
72+
73+
**How can we improve this page:**
74+
${formData.comments || "No additional comments provided"}`;
75+
76+
const issueTitle = `Feedback: ${pageTitle}`;
77+
78+
// Construct GitHub issue URL
79+
const githubUrl = new URL("https://github.com/authzed/docs/issues/new");
80+
githubUrl.searchParams.set("title", issueTitle);
81+
githubUrl.searchParams.set("body", issueBody);
82+
githubUrl.searchParams.set("labels", "suggestion/requested");
83+
84+
// Open in new tab
85+
window.open(githubUrl.toString(), "_blank");
86+
}
87+
88+
setShowForm(false);
89+
setShowThankYou(true);
90+
};
91+
92+
const handleDismiss = () => {
93+
setShowForm(false);
94+
setShowThankYou(true);
95+
};
96+
97+
if (showThankYou) {
98+
return (
99+
<div className={`border-t border-neutral-200 dark:border-neutral-800 py-8 transition-opacity duration-700 ${isVisible ? 'opacity-100' : 'opacity-0'}`}>
100+
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">
101+
Thank you for your feedback!
102+
</div>
103+
</div>
104+
);
105+
}
106+
107+
if (showForm) {
108+
return (
109+
<div className={`border-t border-neutral-200 dark:border-neutral-800 py-8 transition-opacity duration-700 ${isVisible ? 'opacity-100' : 'opacity-0'}`}>
110+
<form onSubmit={handleFormSubmit} className="space-y-4">
111+
<div>
112+
<div className="text-sm mb-3 font-semibold text-gray-900 dark:text-gray-100">
113+
{isHelpful ? "What did you find helpful?" : "What went wrong?"}
114+
</div>
115+
<div className="space-y-2">
116+
{isHelpful ? (
117+
<>
118+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
119+
<input
120+
type="checkbox"
121+
checked={formData.hadRightInfo}
122+
onChange={(e) => setFormData({ ...formData, hadRightInfo: e.target.checked })}
123+
className="cursor-pointer"
124+
/>
125+
Had the right info
126+
</label>
127+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
128+
<input
129+
type="checkbox"
130+
checked={formData.easyToUnderstand}
131+
onChange={(e) => setFormData({ ...formData, easyToUnderstand: e.target.checked })}
132+
className="cursor-pointer"
133+
/>
134+
Easy to understand
135+
</label>
136+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
137+
<input
138+
type="checkbox"
139+
checked={formData.somethingElse}
140+
onChange={(e) => setFormData({ ...formData, somethingElse: e.target.checked })}
141+
className="cursor-pointer"
142+
/>
143+
Something else
144+
</label>
145+
</>
146+
) : (
147+
<>
148+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
149+
<input
150+
type="checkbox"
151+
checked={formData.missingInfo}
152+
onChange={(e) => setFormData({ ...formData, missingInfo: e.target.checked })}
153+
className="cursor-pointer"
154+
/>
155+
Missing info
156+
</label>
157+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
158+
<input
159+
type="checkbox"
160+
checked={formData.difficultToUnderstand}
161+
onChange={(e) => setFormData({ ...formData, difficultToUnderstand: e.target.checked })}
162+
className="cursor-pointer"
163+
/>
164+
Difficult to understand
165+
</label>
166+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
167+
<input
168+
type="checkbox"
169+
checked={formData.containsErrors}
170+
onChange={(e) => setFormData({ ...formData, containsErrors: e.target.checked })}
171+
className="cursor-pointer"
172+
/>
173+
Contains errors
174+
</label>
175+
</>
176+
)}
177+
</div>
178+
</div>
179+
180+
<div>
181+
<label className="text-sm font-semibold block mb-2 text-gray-900 dark:text-gray-100">
182+
{isHelpful ? "Comments" : "How can we improve this page?"}
183+
</label>
184+
<textarea
185+
value={formData.comments}
186+
onChange={(e) => setFormData({ ...formData, comments: e.target.value })}
187+
placeholder="Share your suggestions..."
188+
className="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 resize-none focus:outline-none focus:border-gray-400 dark:focus:border-gray-500"
189+
rows={3}
190+
/>
191+
</div>
192+
193+
<div>
194+
<label className="text-sm font-semibold block mb-2 text-gray-900 dark:text-gray-100">
195+
Email
196+
</label>
197+
<input
198+
type="email"
199+
required
200+
value={formData.email}
201+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
202+
placeholder="[email protected]"
203+
className="w-full px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded text-sm text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:border-gray-400 dark:focus:border-gray-500"
204+
/>
205+
</div>
206+
207+
<div className={`flex gap-2 ${!isHelpful ? "flex-col" : ""}`}>
208+
<button
209+
type="submit"
210+
className={`flex ${isHelpful ? "flex-1" : ""} items-center justify-center gap-2 px-4 py-2 rounded text-sm font-medium transition-colors cursor-pointer bg-gray-900 dark:bg-white text-white dark:text-black hover:bg-gray-700 dark:hover:bg-gray-200`}
211+
>
212+
{!isHelpful && <FontAwesomeIcon className="h-4 w-4" icon={faGithub} />}
213+
{isHelpful ? "Submit" : "Create Issue"}
214+
</button>
215+
<button
216+
type="button"
217+
onClick={handleDismiss}
218+
className="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm font-medium text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors cursor-pointer"
219+
>
220+
Dismiss
221+
</button>
222+
</div>
223+
</form>
224+
</div>
225+
);
226+
}
227+
228+
return (
229+
<div className={`border-t border-neutral-200 dark:border-neutral-800 py-8 transition-opacity duration-700 ${isVisible ? 'opacity-100' : 'opacity-0'}`}>
230+
<div className="text-sm mb-4 font-semibold text-gray-900 dark:text-gray-100">
231+
Was this page helpful?
232+
</div>
233+
<div className="flex gap-2">
234+
<button
235+
onClick={handleYes}
236+
className="flex flex-1 items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm font-medium text-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors cursor-pointer"
237+
>
238+
<FontAwesomeIcon className="h-4 w-4 mr-2" icon={faThumbsUp} />
239+
Yes
240+
</button>
241+
<button
242+
onClick={handleNo}
243+
className="flex flex-1 items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded text-sm font-medium text-gray-900 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors cursor-pointer"
244+
>
245+
No
246+
<FontAwesomeIcon className="h-4 w-4 ml-2" icon={faThumbsDown} />
247+
</button>
248+
</div>
249+
</div>
250+
);
251+
}

components/toc-extra-content.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Feedback } from "@/components/feedback";
2+
import { TocCTA } from "@/components/cta";
3+
4+
export function TocExtraContent() {
5+
return (
6+
<>
7+
<TocCTA />
8+
<Feedback />
9+
</>
10+
);
11+
}

0 commit comments

Comments
 (0)