"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { Inbox, Loading } from "@openstatus/icons"; import { Form, FormControl, FormField, FormItem, FormLabel, } from "@openstatus/ui/components/ui/form"; import { Kbd } from "@openstatus/ui/components/ui/kbd"; import { Popover, PopoverContent, PopoverTrigger, } from "@openstatus/ui/components/ui/popover"; import { Textarea } from "@openstatus/ui/components/ui/textarea"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { toastAction } from "../lib/toast"; type Rating = "up" | "down"; const ratingKey = (path: string) => `docs-rating:${path}`; const schema = z.object({ message: z.string().trim().min(1).max(2000), }); type FeedbackBody = | { kind: "rating"; path: string; rating: Rating; previous?: Rating } | { kind: "message"; path: string; message: string }; // never throws; returns whether the server accepted the submission async function postFeedback(body: FeedbackBody): Promise { try { const res = await fetch("/api/feedback/docs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); return res.ok; } catch { return false; } } function DocsFeedbackBar({ path }: { path: string }) { const [rating, setRating] = useState(); const [open, setOpen] = useState(false); const [sent, setSent] = useState(false); const form = useForm>({ resolver: zodResolver(schema), defaultValues: { message: "" }, }); // rehydrate a prior vote so the chosen arrow stays highlighted across loads useEffect(() => { const stored = window.localStorage.getItem(ratingKey(path)); if (stored === "up" || stored === "down") setRating(stored); }, [path]); // reset the popover contents shortly after it closes (300ms = close anim) useEffect(() => { if (!open && sent) { const t = setTimeout(() => { setSent(false); form.reset(); }, 300); return () => clearTimeout(t); } }, [open, sent, form]); function rate(value: Rating) { // read the prior vote from localStorage (not state) so a click before the // hydration effect still reports the correct `previous` to the server const stored = window.localStorage.getItem(ratingKey(path)); const previous = stored === "up" || stored === "down" ? stored : undefined; if (previous === value) return; setRating(value); window.localStorage.setItem(ratingKey(path), value); void postFeedback({ kind: "rating", path, rating: value, previous }); } async function onSubmit(values: z.infer) { const ok = await postFeedback({ kind: "message", path, message: values.message, }); if (ok) { setSent(true); } else { toastAction("error"); } } return (

Was this helpful?

{sent ? (

Thanks for sharing!

We read every note.

) : (
( Feedback