Add feedback dialog for cloud dashboard (#293)
This commit is contained in:
@@ -13,6 +13,7 @@ import { dashboardRouter } from "./routers/dashboard";
|
||||
import { suppressionRouter } from "./routers/suppression";
|
||||
import { limitsRouter } from "./routers/limits";
|
||||
import { waitlistRouter } from "./routers/waitlist";
|
||||
import { feedbackRouter } from "./routers/feedback";
|
||||
|
||||
/**
|
||||
* This is the primary router for your server.
|
||||
@@ -34,6 +35,7 @@ export const appRouter = createTRPCRouter({
|
||||
suppression: suppressionRouter,
|
||||
limits: limitsRouter,
|
||||
waitlist: waitlistRouter,
|
||||
feedback: feedbackRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -10,6 +10,7 @@ import { sendMail } from "~/server/mailer";
|
||||
import { logger } from "~/server/logger/log";
|
||||
import { UseSend } from "usesend-js";
|
||||
import { isCloud } from "~/utils/common";
|
||||
import { toPlainHtml } from "~/server/utils/email-content";
|
||||
|
||||
const waitlistUserSelection = {
|
||||
id: true,
|
||||
@@ -19,17 +20,6 @@ const waitlistUserSelection = {
|
||||
createdAt: true,
|
||||
} as const;
|
||||
|
||||
function toPlainHtml(text: string) {
|
||||
const escaped = text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
return `<pre style="font-family: inherit; white-space: pre-wrap; margin: 0;">${escaped}</pre>`;
|
||||
}
|
||||
|
||||
function formatDisplayNameFromEmail(email: string) {
|
||||
const localPart = email.split("@")[0] ?? email;
|
||||
const pieces = localPart.split(/[._-]+/).filter(Boolean);
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { z } from "zod";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { createTRPCRouter, teamProcedure } from "~/server/api/trpc";
|
||||
import { env } from "~/env";
|
||||
import { isCloud } from "~/utils/common";
|
||||
import { sendMail } from "~/server/mailer";
|
||||
import { toPlainHtml } from "~/server/utils/email-content";
|
||||
|
||||
export const feedbackRouter = createTRPCRouter({
|
||||
send: teamProcedure
|
||||
.input(
|
||||
z.object({
|
||||
message: z.string().trim().min(1, "Feedback cannot be empty").max(2000),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (!isCloud()) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Feedback is only available on the cloud version.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!env.FOUNDER_EMAIL) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Feedback email is not configured.",
|
||||
});
|
||||
}
|
||||
|
||||
const senderEmail = ctx.session.user.email ?? "Unknown";
|
||||
const senderName = ctx.session.user.name ?? "Unknown";
|
||||
|
||||
const text = `New feedback received\n\nFrom: ${senderName} (${senderEmail})\nUser ID: ${ctx.session.user.id}\nTeam: ${ctx.team.name} (ID: ${ctx.team.id})\n\nMessage:\n${
|
||||
input.message
|
||||
}`;
|
||||
|
||||
await sendMail(
|
||||
env.FOUNDER_EMAIL,
|
||||
`Product feedback from ${ctx.team.name}`,
|
||||
text,
|
||||
toPlainHtml(text),
|
||||
ctx.session.user.email ?? undefined,
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
WAITLIST_EMAIL_TYPES,
|
||||
waitlistSubmissionSchema,
|
||||
} from "~/app/wait-list/schema";
|
||||
import { escapeHtml } from "~/server/utils/email-content";
|
||||
|
||||
const RATE_LIMIT_WINDOW_SECONDS = 60 * 60 * 6; // 6 hours
|
||||
const RATE_LIMIT_MAX_ATTEMPTS = 3;
|
||||
@@ -18,15 +19,6 @@ const EMAIL_TYPE_LABEL: Record<(typeof WAITLIST_EMAIL_TYPES)[number], string> =
|
||||
marketing: "Marketing",
|
||||
};
|
||||
|
||||
function escapeHtml(input: string) {
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export const waitlistRouter = createTRPCRouter({
|
||||
submitRequest: authedProcedure
|
||||
.input(waitlistSubmissionSchema)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export function escapeHtml(input: string) {
|
||||
return input
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export function toPlainHtml(text: string) {
|
||||
return `<pre style="font-family: inherit; white-space: pre-wrap; margin: 0;">${escapeHtml(text)}</pre>`;
|
||||
}
|
||||
Reference in New Issue
Block a user