From c103e0162e3d456a83c29935cb5006a1a7df3d97 Mon Sep 17 00:00:00 2001 From: setkyar Date: Tue, 25 Aug 2026 22:10:48 +0700 Subject: [PATCH 1/2] feat(web): add feedback widget Add a dismissible floating "Feedback" tab that opens a dialog posting to the public POST /api/feedback endpoint. Includes EN/ZH-CN translations and a plain-fetch client (no auth/tenant headers, matching the endpoint's public nature). --- apps/web/src/App.tsx | 2 + .../components/feedback/FeedbackWidget.tsx | 181 ++++++++++++++++++ apps/web/src/components/feedback/index.ts | 1 + apps/web/src/lib/api/feedback.ts | 49 +++++ apps/web/src/locales/en.json | 14 ++ apps/web/src/locales/zh-CN.json | 14 ++ 6 files changed, 261 insertions(+) create mode 100644 apps/web/src/components/feedback/FeedbackWidget.tsx create mode 100644 apps/web/src/components/feedback/index.ts create mode 100644 apps/web/src/lib/api/feedback.ts diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 9e3df255..c75c0a18 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -10,6 +10,7 @@ import { ProtectedRoute, WorkspaceRouteGuard, } from "./components/auth"; +import { FeedbackWidget } from "./components/feedback"; import { ProtectedAppLayout } from "./components/layout/ProtectedAppLayout"; import { KeyboardShortcutsModal } from "./components/settings"; import { PageSkeleton } from "./components/ui"; @@ -296,6 +297,7 @@ function App() { + ); diff --git a/apps/web/src/components/feedback/FeedbackWidget.tsx b/apps/web/src/components/feedback/FeedbackWidget.tsx new file mode 100644 index 00000000..4f30aa35 --- /dev/null +++ b/apps/web/src/components/feedback/FeedbackWidget.tsx @@ -0,0 +1,181 @@ +import { X } from "lucide-react"; +import { useState, type FormEvent } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { submitFeedback } from "@/lib/api/feedback"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; + +const FEEDBACK_DISMISSED_KEY = "wateaminbox-feedback-dismissed"; +const FEEDBACK_MIN_LENGTH = 10; +const FEEDBACK_MAX_LENGTH = 5000; + +function readDismissed(): boolean { + try { + return localStorage.getItem(FEEDBACK_DISMISSED_KEY) === "1"; + } catch { + return false; + } +} + +/** + * Floating feedback widget. + * + * Renders a vertical "Feedback" tab docked to the right edge of the viewport. + * Clicking the tab opens a dialog whose form posts to the public `/api/feedback` + * endpoint. The × button dismisses the tab permanently for this browser. + */ +export function FeedbackWidget() { + const { t } = useTranslation(); + const [dismissed, setDismissed] = useState(readDismissed); + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(""); + const [email, setEmail] = useState(""); + const [submitting, setSubmitting] = useState(false); + + if (dismissed) { + return null; + } + + const handleDismiss = () => { + setDismissed(true); + try { + localStorage.setItem(FEEDBACK_DISMISSED_KEY, "1"); + } catch { + // Ignore storage failures; the tab will simply return on next reload. + } + }; + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + + if (message.trim().length < FEEDBACK_MIN_LENGTH) { + toast.error( + t("feedback.minLength", "Feedback must be at least 10 characters."), + ); + return; + } + + setSubmitting(true); + try { + await submitFeedback({ + message: message.trim(), + email: email.trim() || undefined, + }); + toast.success(t("feedback.success", "Thank you for your feedback!")); + setOpen(false); + setMessage(""); + setEmail(""); + } catch (error) { + toast.error( + error instanceof Error + ? error.message + : t( + "feedback.error", + "Failed to submit feedback. Please try again later.", + ), + ); + } finally { + setSubmitting(false); + } + }; + + return ( + <> +
+
+ + +
+
+ + + + + {t("feedback.title", "Send feedback")} + + {t( + "feedback.description", + "Tell us what's working and what we can improve.", + )} + + + +
+
+ +