diff --git a/integrations/emails.tsx b/integrations/emails.tsx index 31ca6d0e..c3f3993f 100644 --- a/integrations/emails.tsx +++ b/integrations/emails.tsx @@ -273,6 +273,38 @@ staple.helpdesk@gmail.com } } +export function createFeedbackMsg({ + fromUsername, + fromEmail, + subject, + message, +}: { + fromUsername: string + fromEmail: string + subject: string + message: string +}) { + return { + from: "STAPLE ", + to: "staple.helpdesk@gmail.com", + replyTo: fromEmail ? `${fromUsername} <${fromEmail}>` : "STAPLE App", + subject: `[Feedback] ${subject}`, + html: ` + + +
STAPLE Logo
+

Feedback from ${fromUsername}

+

From: ${fromUsername} <${fromEmail}>

+

Subject: ${subject}

+

Message:

+

${message.replace(/\n/g, "
")}

+ + + `, + } +} + export function createEditProfileMsg(user) { return { from: "STAPLE ", diff --git a/src/core/components/FeedbackButton.tsx b/src/core/components/FeedbackButton.tsx new file mode 100644 index 00000000..a9e453d2 --- /dev/null +++ b/src/core/components/FeedbackButton.tsx @@ -0,0 +1,78 @@ +import { useRef } from "react" +import { useMutation } from "@blitzjs/rpc" +import { QuestionMarkCircleIcon } from "@heroicons/react/24/outline" +import { z } from "zod" +import sendFeedback from "src/core/mutations/sendFeedback" +import toast from "react-hot-toast" +import { Form } from "src/core/components/fields/Form" +import { LabeledTextField } from "src/core/components/fields/LabeledTextField" +import LabeledTextAreaField from "src/core/components/fields/LabeledTextAreaField" + +const FeedbackSchema = z.object({ + subject: z.string().min(1, "Subject is required").max(200), + message: z.string().min(1, "Message is required").max(5000), +}) + +export const FeedbackButton = () => { + const [sendFeedbackMutation] = useMutation(sendFeedback) + const dialogRef = useRef(null) + + const openModal = () => dialogRef.current?.showModal() + const closeModal = () => dialogRef.current?.close() + + const handleSubmit = async (values: { subject: string; message: string }) => { + try { + await sendFeedbackMutation(values) + toast.success("Feedback sent! We'll be in touch.") + closeModal() + } catch { + toast.error("Failed to send feedback. Please try again.") + } + } + + return ( + <> + + + { + if (e.target === dialogRef.current) closeModal() + }} + > +
e.stopPropagation()}> +

Send Feedback

+
+ + + +
+
+ + ) +} diff --git a/src/core/layouts/Layout.tsx b/src/core/layouts/Layout.tsx index 93ec3c5b..71369b17 100644 --- a/src/core/layouts/Layout.tsx +++ b/src/core/layouts/Layout.tsx @@ -6,6 +6,7 @@ import Sidebar from "../components/sidebar/Sidebar" import { Toaster } from "react-hot-toast" import useSidebar from "src/core/hooks/useSidebar" import useExpanded from "src/core/hooks/useExpanded" +import { FeedbackButton } from "../components/FeedbackButton" const Layout: BlitzLayout<{ title?: string @@ -17,7 +18,7 @@ const Layout: BlitzLayout<{ return ( <> - + {/* @ts-expect-error false positive: JSX children are valid here */} {title || "STAPLE"} @@ -33,6 +34,7 @@ const Layout: BlitzLayout<{
{children}
+ ) } diff --git a/src/core/mutations/sendFeedback.ts b/src/core/mutations/sendFeedback.ts new file mode 100644 index 00000000..2960b536 --- /dev/null +++ b/src/core/mutations/sendFeedback.ts @@ -0,0 +1,32 @@ +import { resolver } from "@blitzjs/rpc" +import { z } from "zod" +import db from "db" +import { ResendMsg } from "integrations/mailer" +import { createFeedbackMsg } from "integrations/emails" + +const SendFeedbackInput = z.object({ + subject: z.string().min(1).max(200), + message: z.string().min(1).max(5000), +}) + +export default resolver.pipe( + resolver.zod(SendFeedbackInput), + resolver.authorize(), + async ({ subject, message }, ctx) => { + const user = await db.user.findFirst({ + where: { id: ctx.session.userId! }, + select: { username: true, email: true }, + }) + + await ResendMsg( + createFeedbackMsg({ + fromUsername: user?.username ?? "Unknown", + fromEmail: user?.email ?? "Unknown", + subject, + message, + }) + ) + + return { success: true } + } +)