From 29ea93ad6d6552df49c296d88bffc1a875db3948 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:17:49 -0700 Subject: [PATCH] Add support page with Discord link and contact form, fixes #233 (#234) - New /support route with Discord join button and contact form - Contact form sends via Resend to support@brackt.com (email stays hidden from users) - Spam protection: honeypot field + Cloudflare Turnstile verification - Server-side input length limits (subject: 200, message: 5000 chars) - Added Support nav link to navbar (desktop + mobile) Fixes #233 Co-authored-by: Claude Sonnet 4.6 --- .env.example | 3 + app/components/navbar.tsx | 12 +++ app/routes.ts | 1 + app/routes/support.tsx | 215 ++++++++++++++++++++++++++++++++++++++ package-lock.json | 48 ++++++++- package.json | 2 + 6 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 app/routes/support.tsx diff --git a/.env.example b/.env.example index 9cb92d7..3afec4a 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,6 @@ CONTAINER_REGISTRY="" DEV_ADMIN_CLERK_ID="" SENTRY_AUTH_TOKEN="" SQUIGGLE_CONTACT_EMAIL="" +RESEND_API_KEY="" +TURNSTILE_SITE_KEY=1x00000000000000000000AA +TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA \ No newline at end of file diff --git a/app/components/navbar.tsx b/app/components/navbar.tsx index b582ff9..6e83d40 100644 --- a/app/components/navbar.tsx +++ b/app/components/navbar.tsx @@ -50,6 +50,11 @@ export function Navbar({ isAdmin }: NavbarProps) { Rules + + + Support + + {isAdmin && ( @@ -122,6 +127,13 @@ export function Navbar({ isAdmin }: NavbarProps) { > Rules + setOpen(false)} + > + Support + {isAdmin && ( SUBJECT_MAX) { + return data({ error: `Subject must be ${SUBJECT_MAX} characters or fewer.` }); + } + if (message.length > MESSAGE_MAX) { + return data({ error: `Message must be ${MESSAGE_MAX} characters or fewer.` }); + } + + // Turnstile verification + const turnstileSecret = process.env.TURNSTILE_SECRET_KEY; + if (turnstileSecret) { + if (!turnstileToken) { + return data({ error: "Please complete the verification." }); + } + const verifyResponse = await fetch( + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + secret: turnstileSecret, + response: turnstileToken, + }), + } + ); + const verifyResult = (await verifyResponse.json()) as { success: boolean }; + if (!verifyResult.success) { + return data({ error: "Verification failed. Please try again." }); + } + } + + // Send email via Resend + const resendKey = process.env.RESEND_API_KEY; + if (!resendKey) { + return data({ error: "Server configuration error. Please try again later." }); + } + + const resend = new Resend(resendKey); + const { error: sendError } = await resend.emails.send({ + from: "Brackt Support Form ", + to: "support@brackt.com", + replyTo: email, + subject: `[Support] ${subject}`, + text: `Name: ${name}\nEmail: ${email}\n\n${message}`, + }); + + if (sendError) { + return data({ error: "Failed to send your message. Please try again." }); + } + + return data({ success: true as const }); +} + +export default function Support() { + const { turnstileSiteKey } = useLoaderData(); + const actionData = useActionData(); + const success = actionData && "success" in actionData; + const error = actionData && "error" in actionData ? actionData.error : null; + + return ( +
+

Support

+

+ Need help? Join our Discord community for the fastest response, or send + us a message directly. +

+ +
+ {/* Discord */} +
+
+ +

Discord Community

+
+

+ The fastest way to get help is to join our Discord server. You can + ask questions, report bugs, and chat with other Brackt players. +

+ +
+ + {/* Contact Form */} +
+
+ +

Send a Message

+
+

+ Prefer email? Fill out the form below and we'll get back to you. +

+ + {success ? ( +
+ +

Message sent! We'll get back to you as soon as we can.

+
+ ) : ( +
+ {/* Honeypot — visually and positionally hidden; not using display:none so bots can't trivially detect it */} + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +