From c0a40cede4b2eeaa069a5922fae03e4d271bfcff Mon Sep 17 00:00:00 2001 From: Justin Levine <20596508+jal-co@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:06:31 -0400 Subject: [PATCH] feat(feedback): add categories and UI element picker Signed-off-by: Justin Levine <20596508+jal-co@users.noreply.github.com> --- apps/web/src/components/app-shell.tsx | 4 +- apps/web/src/components/feedback-dialog.tsx | 79 ++++++++++++-- apps/web/src/lib/element-picker.ts | 112 ++++++++++++++++++++ apps/web/src/lib/feedback.ts | 23 +++- 4 files changed, 207 insertions(+), 11 deletions(-) create mode 100644 apps/web/src/lib/element-picker.ts diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index fd5514e..8862833 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -199,10 +199,10 @@ function UserMenu({ user }: { user: CurrentUser }) { busy={feedbackBusy} done={feedbackDone} error={feedbackError} - onSubmit={(message) => { + onSubmit={({ message, category, element }) => { setFeedbackBusy(true); setFeedbackError(null); - void submitFeedback({ data: { message, page: window.location.pathname } }).then((res) => { + void submitFeedback({ data: { message, category, element, page: window.location.pathname } }).then((res) => { setFeedbackBusy(false); if (!res.ok) { setFeedbackError(res.error ?? "Could not send."); diff --git a/apps/web/src/components/feedback-dialog.tsx b/apps/web/src/components/feedback-dialog.tsx index b4ae22a..64de756 100644 --- a/apps/web/src/components/feedback-dialog.tsx +++ b/apps/web/src/components/feedback-dialog.tsx @@ -1,9 +1,10 @@ /** * Feedback dialog — a short message straight to the team (Discord). - * Presentational: the caller owns submission. + * Presentational: the caller owns submission. When the UI category is + * selected, an optional element picker captures the exact element context. */ import { useEffect, useRef, useState } from "react"; -import { HiCheck as Check } from "react-icons/hi2"; +import { HiCheck as Check, HiCursorArrowRays as Picker, HiXMark as X } from "react-icons/hi2"; import { Button } from "@/components/ui/button"; import { @@ -15,6 +16,23 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { pickElement, type PickedElement } from "@/lib/element-picker"; + +export const FEEDBACK_CATEGORIES = [ + { id: "bug", label: "Bug" }, + { id: "idea", label: "Idea" }, + { id: "ui", label: "UI issue" }, + { id: "other", label: "Other" }, +] as const; + +export type FeedbackCategory = (typeof FEEDBACK_CATEGORIES)[number]["id"]; + +export interface FeedbackSubmission { + message: string; + category: FeedbackCategory; + element: PickedElement | null; +} export function FeedbackDialog({ open, @@ -26,21 +44,38 @@ export function FeedbackDialog({ }: { open: boolean; onOpenChange: (open: boolean) => void; - onSubmit: (message: string) => void; + onSubmit: (submission: FeedbackSubmission) => void; busy?: boolean; done?: boolean; error?: string | null; }) { const [message, setMessage] = useState(""); + const [category, setCategory] = useState("bug"); + const [element, setElement] = useState(null); + const [picking, setPicking] = useState(false); const closeRef = useRef(null); const valid = message.trim().length >= 4; useEffect(() => { if (done) closeRef.current?.focus(); }, [done]); + async function startPicking() { + setPicking(true); + try { + const picked = await pickElement(); + if (picked) setElement(picked); + } finally { + setPicking(false); + } + } + return ( - - + { if (!picking) onOpenChange(next); }} modal={!picking}> + { if (picking) event.preventDefault(); }} + onInteractOutside={(event) => { if (picking) event.preventDefault(); }} + > Send feedback @@ -51,7 +86,7 @@ export function FeedbackDialog({ aria-busy={busy} onSubmit={(event) => { event.preventDefault(); - if (valid && !busy) onSubmit(message.trim()); + if (valid && !busy) onSubmit({ message: message.trim(), category, element: category === "ui" ? element : null }); }} >
@@ -60,6 +95,35 @@ export function FeedbackDialog({
) : ( +
+
+ + +
+ {category === "ui" ? ( +
+ {element ? ( +
+
+

{element.selector}

+

{element.text || element.rect}

+
+ +
+ ) : ( + + )} +
+ ) : null}