187 lines
4.5 KiB
TypeScript
187 lines
4.5 KiB
TypeScript
"use client";
|
|
import { cva } from "class-variance-authority";
|
|
import { buttonVariants } from "fumadocs-ui/components/ui/button";
|
|
import {
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
} from "fumadocs-ui/components/ui/collapsible";
|
|
import { ThumbsDown, ThumbsUp } from "lucide-react";
|
|
import { usePathname } from "next/navigation";
|
|
import posthog from "posthog-js";
|
|
import { type SyntheticEvent, useEffect, useState, useTransition } from "react";
|
|
import { cn } from "@/lib/cn";
|
|
|
|
const rateButtonVariants = cva(
|
|
"inline-flex items-center gap-2 px-3 py-2 rounded-full font-medium border text-sm [&_svg]:size-4 disabled:cursor-not-allowed",
|
|
{
|
|
variants: {
|
|
active: {
|
|
true: "bg-fd-accent text-fd-accent-foreground [&_svg]:fill-current",
|
|
false: "text-fd-muted-foreground",
|
|
},
|
|
},
|
|
},
|
|
);
|
|
|
|
export interface Feedback {
|
|
opinion: "good" | "bad";
|
|
url?: string;
|
|
message: string;
|
|
}
|
|
|
|
export interface ActionResponse {
|
|
githubUrl: string;
|
|
}
|
|
|
|
interface Result extends Feedback {
|
|
response?: ActionResponse;
|
|
}
|
|
|
|
export function Rate() {
|
|
const url = usePathname();
|
|
const [previous, setPrevious] = useState<Result | null>(null);
|
|
const [opinion, setOpinion] = useState<"good" | "bad" | null>(null);
|
|
const [message, setMessage] = useState("");
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
useEffect(() => {
|
|
const item = localStorage.getItem(`docs-feedback-${url}`);
|
|
|
|
if (item === null) return;
|
|
setPrevious(JSON.parse(item) as Result);
|
|
}, [url]);
|
|
|
|
useEffect(() => {
|
|
const key = `docs-feedback-${url}`;
|
|
|
|
if (previous) localStorage.setItem(key, JSON.stringify(previous));
|
|
else localStorage.removeItem(key);
|
|
}, [previous, url]);
|
|
|
|
function submit(e?: SyntheticEvent) {
|
|
if (opinion == null) return;
|
|
|
|
startTransition(async () => {
|
|
const feedback: Feedback = {
|
|
url,
|
|
opinion,
|
|
message,
|
|
};
|
|
posthog.capture("on_rate_docs", feedback);
|
|
setPrevious(feedback);
|
|
setMessage("");
|
|
setOpinion(null);
|
|
});
|
|
|
|
e?.preventDefault();
|
|
}
|
|
|
|
const activeOpinion = previous?.opinion ?? opinion;
|
|
|
|
return (
|
|
<Collapsible
|
|
open={opinion !== null || previous !== null}
|
|
onOpenChange={(v) => {
|
|
if (!v) setOpinion(null);
|
|
}}
|
|
className="border-y py-3"
|
|
>
|
|
<div className="flex flex-row items-center gap-2">
|
|
<p className="text-sm font-medium pe-2">How is this guide?</p>
|
|
<button
|
|
type="button"
|
|
disabled={previous !== null}
|
|
className={cn(
|
|
rateButtonVariants({
|
|
active: activeOpinion === "good",
|
|
}),
|
|
)}
|
|
onClick={() => {
|
|
setOpinion("good");
|
|
}}
|
|
>
|
|
<ThumbsUp />
|
|
Good
|
|
</button>
|
|
<button
|
|
type="button"
|
|
disabled={previous !== null}
|
|
className={cn(
|
|
rateButtonVariants({
|
|
active: activeOpinion === "bad",
|
|
}),
|
|
)}
|
|
onClick={() => {
|
|
setOpinion("bad");
|
|
}}
|
|
>
|
|
<ThumbsDown />
|
|
Bad
|
|
</button>
|
|
</div>
|
|
<CollapsibleContent className="mt-3">
|
|
{previous ? (
|
|
<div className="px-3 py-6 flex flex-col items-center gap-3 bg-fd-card text-fd-muted-foreground text-sm text-center rounded-xl">
|
|
<p>Thank you for your feedback!</p>
|
|
<div className="flex flex-row items-center gap-2">
|
|
<a
|
|
href={previous.response?.githubUrl}
|
|
rel="noreferrer noopener"
|
|
target="_blank"
|
|
className={cn(
|
|
buttonVariants({
|
|
color: "primary",
|
|
}),
|
|
"text-xs",
|
|
)}
|
|
>
|
|
View on GitHub
|
|
</a>
|
|
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
buttonVariants({
|
|
color: "secondary",
|
|
}),
|
|
"text-xs",
|
|
)}
|
|
onClick={() => {
|
|
setOpinion(previous.opinion);
|
|
setPrevious(null);
|
|
}}
|
|
>
|
|
Submit Again
|
|
</button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<form className="flex flex-col gap-3" onSubmit={submit}>
|
|
<textarea
|
|
// biome-ignore lint/a11y/noAutofocus: this is a controlled, internal form
|
|
autoFocus
|
|
required
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
className="border rounded-lg bg-fd-secondary text-fd-secondary-foreground p-3 resize-none focus-visible:outline-none placeholder:text-fd-muted-foreground"
|
|
placeholder="Leave your feedback..."
|
|
onKeyDown={(e) => {
|
|
if (!e.shiftKey && e.key === "Enter") {
|
|
submit(e);
|
|
}
|
|
}}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className={cn(buttonVariants({ color: "outline" }), "w-fit px-3")}
|
|
disabled={isPending}
|
|
>
|
|
Submit
|
|
</button>
|
|
</form>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
);
|
|
}
|