Skip to content

Commit 744f492

Browse files
authored
Merge pull request #485 from authzed/suggestion-ui
Add feedback widget and update CTA
2 parents 576c43d + 9ba0f54 commit 744f492

4 files changed

Lines changed: 339 additions & 22 deletions

File tree

app/layout.tsx

Lines changed: 3 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";
@@ -88,15 +88,9 @@ export default async function RootLayout({ children }) {
8888
}}
8989
pageMap={pageMap}
9090
feedback={{
91-
content: (
92-
<span>
93-
Something unclear?
94-
<br />
95-
Create an issue →
96-
</span>
97-
),
91+
content: null,
9892
}}
99-
toc={{ backToTop: true, extraContent: <TocCTA /> }}
93+
toc={{ backToTop: true, extraContent: <TocExtraContent /> }}
10094
>
10195
<OurLayout>
10296
<SpeedInsights />

components/cta.tsx

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
"use client";
22

33
import { Button } from "@/components/ui/button";
4-
import { faPhone } from "@fortawesome/free-solid-svg-icons";
5-
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
64
import Link from "next/link";
75
import { usePathname } from "next/navigation";
86

@@ -11,18 +9,27 @@ export function TocCTA() {
119
const isCommercial = pathname?.startsWith("/authzed/");
1210

1311
return isCommercial ? (
14-
<div className="flex flex-wrap w-full nx-mt-8 nx-border-t nx-bg-white nx-pt-8 nx-shadow-[0_-12px_16px_white] dark:nx-bg-dark dark:nx-shadow-[0_-12px_16px_#111] nx-sticky nx-bottom-0 nx-flex nx-flex-col nx-items-start nx-gap-2 nx-pb-8 dark:nx-border-neutral-800 contrast-more:nx-border-t contrast-more:nx-border-neutral-400 contrast-more:nx-shadow-none contrast-more:dark:nx-border-neutral-400">
15-
<div className="nx-text-xs">Talk to us</div>
16-
<div>
17-
<Link href="https://authzed.com/call?utm_source=docs">
18-
<Button variant="default" size="sm">
19-
Schedule a Call
20-
<FontAwesomeIcon className=" ml-2 h-4 w-4" icon={faPhone} />
21-
</Button>
22-
</Link>
23-
</div>
12+
<div className="mt-10 pt-4 pb-8">
13+
<div className="text-sm mb-4 font-semibold">Explore your use case</div>
14+
<Link href="https://authzed.com/schedule-demo" className="cursor-pointer">
15+
<Button variant="default" size="sm" className="w-full cursor-pointer">
16+
Book a demo
17+
</Button>
18+
</Link>
2419
</div>
2520
) : (
26-
<></>
21+
<div className="mt-10 pt-4 pb-4">
22+
<div className="text-sm mb-1 font-semibold">AuthZed Cloud</div>
23+
<div className="text-sm mb-4 font-normal text-gray-400">Hosted, self-service SpiceDB</div>
24+
<Link href="https://authzed.com/cloud/signup" className="cursor-pointer">
25+
<Button
26+
variant="outline"
27+
size="sm"
28+
className="w-full cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800"
29+
>
30+
Get Started
31+
</Button>
32+
</Link>
33+
</div>
2734
);
2835
}

components/feedback.tsx

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
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+
accurateInfo: false,
16+
solvedMyProblem: false,
17+
easyToUnderstand: false,
18+
somethingElse: false,
19+
inaccurate: false,
20+
couldntFind: false,
21+
difficultToUnderstand: false,
22+
codeSampleError: false,
23+
other: false,
24+
comments: "",
25+
email: "",
26+
});
27+
28+
useEffect(() => {
29+
const timer = setTimeout(() => {
30+
setIsVisible(true);
31+
}, 3000);
32+
33+
return () => clearTimeout(timer);
34+
}, []);
35+
36+
const handleYes = () => {
37+
posthog.capture("docs-feedback-vote", { helpful: true });
38+
setIsHelpful(true);
39+
setShowForm(true);
40+
};
41+
42+
const handleNo = () => {
43+
posthog.capture("docs-feedback-vote", { helpful: false });
44+
setIsHelpful(false);
45+
setShowForm(true);
46+
};
47+
48+
const handleFormSubmit = (e: React.FormEvent) => {
49+
e.preventDefault();
50+
51+
if (formData.email) {
52+
posthog.identify(formData.email);
53+
}
54+
55+
posthog.capture("docs-feedback-detail", {
56+
helpful: isHelpful,
57+
details: formData,
58+
});
59+
60+
// If negative feedback, create GitHub issue
61+
if (!isHelpful) {
62+
const pageUrl = window.location.href;
63+
const pageTitle = document.title;
64+
65+
// Build issue body
66+
const issues: string[] = [];
67+
if (formData.inaccurate) issues.push("Inaccurate");
68+
if (formData.couldntFind) issues.push("Couldn't find what I was looking for");
69+
if (formData.difficultToUnderstand) issues.push("Difficult to understand");
70+
if (formData.codeSampleError) issues.push("Code sample error");
71+
if (formData.other) issues.push("Other");
72+
73+
const issueBody = `**Page:** ${pageUrl}
74+
75+
**What went wrong:**
76+
${issues.map((issue) => `- ${issue}`).join("\n")}
77+
78+
**How can we improve this page:**
79+
${formData.comments || "No additional comments provided"}`;
80+
81+
const issueTitle = `Feedback: ${pageTitle}`;
82+
83+
// Construct GitHub issue URL
84+
const githubUrl = new URL("https://github.com/authzed/docs/issues/new");
85+
githubUrl.searchParams.set("title", issueTitle);
86+
githubUrl.searchParams.set("body", issueBody);
87+
githubUrl.searchParams.set("labels", "suggestion/requested");
88+
89+
// Open in new tab
90+
window.open(githubUrl.toString(), "_blank");
91+
}
92+
93+
setShowForm(false);
94+
setShowThankYou(true);
95+
};
96+
97+
const handleDismiss = () => {
98+
setShowForm(false);
99+
setShowThankYou(true);
100+
};
101+
102+
if (showThankYou) {
103+
return (
104+
<div
105+
className={`border-t border-neutral-200 dark:border-neutral-800 pt-4 pb-8 transition-opacity duration-700 ${isVisible ? "opacity-100" : "opacity-0"}`}
106+
>
107+
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">
108+
Thank you for your feedback!
109+
</div>
110+
</div>
111+
);
112+
}
113+
114+
if (showForm) {
115+
return (
116+
<div
117+
className={`border-t border-neutral-200 dark:border-neutral-800 pt-4 pb-8 transition-opacity duration-700 ${isVisible ? "opacity-100" : "opacity-0"}`}
118+
>
119+
<form onSubmit={handleFormSubmit} className="space-y-4">
120+
<div>
121+
<div className="text-sm mb-3 font-semibold text-gray-900 dark:text-gray-100">
122+
{isHelpful ? "What did you find helpful?" : "What went wrong?"}
123+
</div>
124+
<div className="space-y-2">
125+
{isHelpful ? (
126+
<>
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.accurateInfo}
131+
onChange={(e) => setFormData({ ...formData, accurateInfo: e.target.checked })}
132+
className="cursor-pointer"
133+
/>
134+
Accurate info
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.solvedMyProblem}
140+
onChange={(e) =>
141+
setFormData({ ...formData, solvedMyProblem: e.target.checked })
142+
}
143+
className="cursor-pointer"
144+
/>
145+
Solved my problem
146+
</label>
147+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
148+
<input
149+
type="checkbox"
150+
checked={formData.easyToUnderstand}
151+
onChange={(e) =>
152+
setFormData({ ...formData, easyToUnderstand: e.target.checked })
153+
}
154+
className="cursor-pointer"
155+
/>
156+
Easy to understand
157+
</label>
158+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
159+
<input
160+
type="checkbox"
161+
checked={formData.somethingElse}
162+
onChange={(e) =>
163+
setFormData({ ...formData, somethingElse: e.target.checked })
164+
}
165+
className="cursor-pointer"
166+
/>
167+
Something else
168+
</label>
169+
</>
170+
) : (
171+
<>
172+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
173+
<input
174+
type="checkbox"
175+
checked={formData.inaccurate}
176+
onChange={(e) => setFormData({ ...formData, inaccurate: e.target.checked })}
177+
className="cursor-pointer"
178+
/>
179+
Inaccurate
180+
</label>
181+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
182+
<input
183+
type="checkbox"
184+
checked={formData.couldntFind}
185+
onChange={(e) => setFormData({ ...formData, couldntFind: e.target.checked })}
186+
className="cursor-pointer"
187+
/>
188+
Couldn&apos;t find what I was looking for
189+
</label>
190+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
191+
<input
192+
type="checkbox"
193+
checked={formData.difficultToUnderstand}
194+
onChange={(e) =>
195+
setFormData({ ...formData, difficultToUnderstand: e.target.checked })
196+
}
197+
className="cursor-pointer"
198+
/>
199+
Difficult to understand
200+
</label>
201+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
202+
<input
203+
type="checkbox"
204+
checked={formData.codeSampleError}
205+
onChange={(e) =>
206+
setFormData({ ...formData, codeSampleError: e.target.checked })
207+
}
208+
className="cursor-pointer"
209+
/>
210+
Code sample error
211+
</label>
212+
<label className="flex items-center gap-2 text-sm text-gray-900 dark:text-gray-100 cursor-pointer">
213+
<input
214+
type="checkbox"
215+
checked={formData.other}
216+
onChange={(e) => setFormData({ ...formData, other: e.target.checked })}
217+
className="cursor-pointer"
218+
/>
219+
Other
220+
</label>
221+
</>
222+
)}
223+
</div>
224+
</div>
225+
226+
<div>
227+
<label className="text-sm font-semibold block mb-2 text-gray-900 dark:text-gray-100">
228+
{isHelpful ? "Comments" : "How can we improve this page?"}
229+
</label>
230+
<textarea
231+
value={formData.comments}
232+
onChange={(e) => setFormData({ ...formData, comments: e.target.value })}
233+
placeholder={
234+
isHelpful ? "Tell us more about your experience" : "Share your suggestions..."
235+
}
236+
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"
237+
rows={3}
238+
/>
239+
</div>
240+
241+
<div>
242+
<label className="text-sm font-semibold block mb-1 text-gray-900 dark:text-gray-100">
243+
Email <span className="font-normal text-gray-400 dark:text-gray-500">(optional)</span>
244+
</label>
245+
{!isHelpful && (
246+
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">
247+
We&apos;ll notify you when changes are made in response to your feedback.
248+
</p>
249+
)}
250+
<input
251+
type="email"
252+
value={formData.email}
253+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
254+
placeholder="your.email@example.com"
255+
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"
256+
/>
257+
</div>
258+
259+
<div className={`flex gap-2 ${!isHelpful ? "flex-col" : ""}`}>
260+
<button
261+
type="submit"
262+
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`}
263+
>
264+
{!isHelpful && <FontAwesomeIcon className="h-4 w-4" icon={faGithub} />}
265+
{isHelpful ? "Submit" : "Create Issue"}
266+
</button>
267+
<button
268+
type="button"
269+
onClick={handleDismiss}
270+
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"
271+
>
272+
Dismiss
273+
</button>
274+
</div>
275+
</form>
276+
</div>
277+
);
278+
}
279+
280+
return (
281+
<div
282+
className={`border-t border-neutral-200 dark:border-neutral-800 pt-4 pb-8 transition-opacity duration-700 ${isVisible ? "opacity-100" : "opacity-0"}`}
283+
>
284+
<div className="text-sm mb-4 font-semibold text-gray-900 dark:text-gray-100">
285+
Was this page helpful?
286+
</div>
287+
<div className="flex gap-2">
288+
<button
289+
onClick={handleYes}
290+
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"
291+
>
292+
<FontAwesomeIcon className="h-4 w-4 mr-2" icon={faThumbsUp} />
293+
Yes
294+
</button>
295+
<button
296+
onClick={handleNo}
297+
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"
298+
>
299+
No
300+
<FontAwesomeIcon className="h-4 w-4 ml-2" icon={faThumbsDown} />
301+
</button>
302+
</div>
303+
</div>
304+
);
305+
}

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)