From 089723d748b30824997927c755525c03e72036f7 Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 18 Jul 2025 23:11:46 +0100 Subject: [PATCH] feat: add feedback button and modal --- apps/web/src/components/Dashboard.tsx | 4 + apps/web/src/components/FeedbackButton.tsx | 149 --------------------- apps/web/src/components/FeedbackModal.tsx | 122 +++++++++++++++++ apps/web/src/components/SideNavigation.tsx | 6 - apps/web/src/components/UserMenu.tsx | 34 ++++- apps/web/src/views/board/index.tsx | 11 +- apps/web/src/views/boards/index.tsx | 4 +- apps/web/src/views/card/index.tsx | 5 +- apps/web/src/views/members/index.tsx | 4 +- apps/web/src/views/settings/index.tsx | 4 +- 10 files changed, 181 insertions(+), 162 deletions(-) delete mode 100644 apps/web/src/components/FeedbackButton.tsx create mode 100644 apps/web/src/components/FeedbackModal.tsx diff --git a/apps/web/src/components/Dashboard.tsx b/apps/web/src/components/Dashboard.tsx index 4c47b83c..4df94997 100644 --- a/apps/web/src/components/Dashboard.tsx +++ b/apps/web/src/components/Dashboard.tsx @@ -9,8 +9,11 @@ import { import { authClient } from "@kan/auth/client"; import { useClickOutside } from "~/hooks/useClickOutside"; +import { useModal } from "~/providers/modal"; import { useTheme } from "~/providers/theme"; import { WorkspaceProvider } from "~/providers/workspace"; +import FeedbackModal from "./FeedbackModal"; +import Modal from "./modal"; import SideNavigation from "./SideNavigation"; interface DashboardProps { @@ -39,6 +42,7 @@ export default function Dashboard({ hasRightPanel = false, }: DashboardProps) { const theme = useTheme(); + const { modalContentType } = useModal(); const { data: session, isPending: sessionLoading } = authClient.useSession(); diff --git a/apps/web/src/components/FeedbackButton.tsx b/apps/web/src/components/FeedbackButton.tsx deleted file mode 100644 index d9ed244e..00000000 --- a/apps/web/src/components/FeedbackButton.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import Link from "next/link"; -import { Menu, Transition } from "@headlessui/react"; -import { t } from "@lingui/core/macro"; -import { Fragment, useRef, useState } from "react"; -import { useForm } from "react-hook-form"; - -import chatIconDark from "~/assets/chat-dark.json"; -import chatIconLight from "~/assets/chat-light.json"; -import Button from "~/components/Button"; -import Input from "~/components/Input"; -import LottieIcon from "~/components/LottieIcon"; -import { usePopup } from "~/providers/popup"; -import { useTheme } from "~/providers/theme"; -import { api } from "~/utils/api"; - -interface NewFeedbackFormInput { - feedback: string; -} - -const FeedbackButton: React.FC = () => { - const outsideRef = useRef(null); - const { activeTheme } = useTheme(); - const { showPopup } = usePopup(); - const [isHovered, setIsHovered] = useState(false); - const [index, setIndex] = useState(0); - - const { handleSubmit, setValue, watch, reset } = - useForm({ - defaultValues: { - feedback: "", - }, - }); - - const createFeedback = api.feedback.create.useMutation({ - onSuccess: async () => { - reset(); - showPopup({ - header: t`Feedback sent`, - message: t`Thank you for your feedback!`, - icon: "success", - }); - }, - onError: async () => { - showPopup({ - header: t`Unable to send feedback`, - message: t`Please try again later, or contact customer support.`, - icon: "error", - }); - }, - }); - - const handleMouseEnter = () => { - setIsHovered(true); - setIndex((index) => index + 1); - }; - - const onSubmit = (values: NewFeedbackFormInput) => { - createFeedback.mutate({ - feedback: values.feedback, - url: window.location.href, - }); - }; - - return ( - <> -
- -
- - - {t`Feedback`} - -
- - - -
- { - setValue("feedback", e.target.value); - }} - value={watch("feedback")} - contentEditable - className="max-h-[300px] min-h-[100px]" - onKeyDown={async (e) => { - e.stopPropagation(); - if (e.key === "Enter" && e.shiftKey) { - e.preventDefault(); - await handleSubmit(onSubmit)(); - } - }} - /> -
-
-

- {t`Need help?`}{" "} - - {t`Contact us`} - - {t`, or see our`}{" "} - - {t`docs`} - - . -

-
-
- -
-
-
-
-
-
- - ); -}; - -export default FeedbackButton; diff --git a/apps/web/src/components/FeedbackModal.tsx b/apps/web/src/components/FeedbackModal.tsx new file mode 100644 index 00000000..6f592e8e --- /dev/null +++ b/apps/web/src/components/FeedbackModal.tsx @@ -0,0 +1,122 @@ +import Link from "next/link"; +import { t } from "@lingui/core/macro"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { HiXMark } from "react-icons/hi2"; + +import Button from "~/components/Button"; +import Input from "~/components/Input"; +import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; +import { api } from "~/utils/api"; + +interface NewFeedbackFormInput { + feedback: string; +} + +export default function FeedbackModal() { + const { closeModal } = useModal(); + const { showPopup } = usePopup(); + + const { handleSubmit, setValue, watch, reset } = + useForm({ + defaultValues: { + feedback: "", + }, + }); + + const createFeedback = api.feedback.create.useMutation({ + onSuccess: async () => { + reset(); + closeModal(); + showPopup({ + header: t`Feedback sent`, + message: t`Thank you for your feedback!`, + icon: "success", + }); + }, + onError: async () => { + showPopup({ + header: t`Unable to send feedback`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); + }, + }); + + const onSubmit = (values: NewFeedbackFormInput) => { + createFeedback.mutate({ + feedback: values.feedback, + url: window.location.href, + }); + }; + + return ( +
+
+
+

+ {t`Feedback`} +

+ +
+ + setValue("feedback", e.target.value)} + contentEditable + value={watch("feedback")} + className="min-h-[120px]" + onKeyDown={async (e) => { + e.stopPropagation(); + if (e.key === "Enter" && e.shiftKey) { + e.preventDefault(); + await handleSubmit(onSubmit)(); + } + if (e.key === "Escape") { + closeModal(); + } + }} + /> +
+
+
+

+ {t`Need help?`}{" "} + + {t`Contact us`} + + {t`, or see our`}{" "} + + {t`docs`} + + . +

+
+
+ +
+
+
+ ); +} diff --git a/apps/web/src/components/SideNavigation.tsx b/apps/web/src/components/SideNavigation.tsx index eb7ad628..ef290625 100644 --- a/apps/web/src/components/SideNavigation.tsx +++ b/apps/web/src/components/SideNavigation.tsx @@ -19,7 +19,6 @@ import ReactiveButton from "~/components/ReactiveButton"; import UserMenu from "~/components/UserMenu"; import WorkspaceMenu from "~/components/WorkspaceMenu"; import { useTheme } from "~/providers/theme"; -import FeedbackButton from "./FeedbackButton"; interface SideNavigationProps { user: UserType; @@ -140,11 +139,6 @@ export default function SideNavigation({
- {/* Feedback button above user menu */} - {/*
- -
*/} - { await authClient.signOut(); - router.push("/login"); }; @@ -141,6 +143,36 @@ export default function UserMenu({
+
+ + + {t`Support`} + + + + + {t`Documentation`} + + + + + +
- + + {modalContentType === "NEW_FEEDBACK" && } {modalContentType === "DELETE_BOARD" && ( )} diff --git a/apps/web/src/views/boards/index.tsx b/apps/web/src/views/boards/index.tsx index 8d01bed6..39e4ffa4 100644 --- a/apps/web/src/views/boards/index.tsx +++ b/apps/web/src/views/boards/index.tsx @@ -2,6 +2,7 @@ import { t } from "@lingui/core/macro"; import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2"; import Button from "~/components/Button"; +import FeedbackModal from "~/components/FeedbackModal"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; @@ -51,7 +52,8 @@ export default function BoardsPage() { - + + {modalContentType === "NEW_FEEDBACK" && } {modalContentType === "NEW_BOARD" && } {modalContentType === "IMPORT_BOARDS" && } {modalContentType === "NEW_WORKSPACE" && } diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index e4443b61..a524a592 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -6,12 +6,12 @@ import { IoChevronForwardSharp } from "react-icons/io5"; import Avatar from "~/components/Avatar"; import Editor from "~/components/Editor"; +import FeedbackModal from "~/components/FeedbackModal"; import { LabelForm } from "~/components/LabelForm"; import LabelIcon from "~/components/LabelIcon"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; -import PatternedBackground from "~/components/PatternedBackground"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; import { useWorkspace } from "~/providers/workspace"; @@ -272,7 +272,8 @@ export default function CardPage() { - + + {modalContentType === "NEW_FEEDBACK" && } {modalContentType === "NEW_LABEL" && ( )} diff --git a/apps/web/src/views/members/index.tsx b/apps/web/src/views/members/index.tsx index 24907cb4..c2ae39b8 100644 --- a/apps/web/src/views/members/index.tsx +++ b/apps/web/src/views/members/index.tsx @@ -7,6 +7,7 @@ import { authClient } from "@kan/auth/client"; import Avatar from "~/components/Avatar"; import Button from "~/components/Button"; import Dropdown from "~/components/Dropdown"; +import FeedbackModal from "~/components/FeedbackModal"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; @@ -221,7 +222,8 @@ export default function MembersPage() { - + + {modalContentType === "NEW_FEEDBACK" && } {modalContentType === "NEW_WORKSPACE" && } {modalContentType === "INVITE_MEMBER" && } {modalContentType === "REMOVE_MEMBER" && } diff --git a/apps/web/src/views/settings/index.tsx b/apps/web/src/views/settings/index.tsx index 9cab16e7..b90e9f8d 100644 --- a/apps/web/src/views/settings/index.tsx +++ b/apps/web/src/views/settings/index.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef } from "react"; import { HiMiniArrowTopRightOnSquare } from "react-icons/hi2"; import Button from "~/components/Button"; +import FeedbackModal from "~/components/FeedbackModal"; import { LanguageSelector } from "~/components/LanguageSelector"; import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; @@ -300,7 +301,8 @@ export default function SettingsPage() { - + + {modalContentType === "NEW_FEEDBACK" && } {modalContentType === "NEW_WORKSPACE" && } {modalContentType === "DELETE_WORKSPACE" && (