feat(contact): extract shared ContactForm and add per-subdomain contact pages
Refactor the monolithic main-site contact page into a reusable <ContactForm> component that encapsulates the Turnstile widget, cooldown timer, email-verification flow, and tRPC submission. The main /contact route becomes a thin wrapper that supplies the site-specific disclaimer subline and Life-and-Lineage FAQ accordion (now exported as <LineageContactQuestions> for reuse). Add per-subdomain contact routes (gaze, inputhalo, lineage, nessa) that render the shared component, with site-aware subject prefix, recipient, heading, and PageHead metadata derived via useSite() from CONTACT_CONTEXT. Introduce src/lib/contact-config.ts (with tests) centralizing the contact recipient/sender addresses and subject building. The misc.sendContactRequest mutation accepts an optional subjectPrefix (defaulting to "freno.me") so inbound call sites continue emitting byte-identical legacy subjects, while subdomain submissions route distinct subjects (e.g. "[Gaze] Contact Request") to the same inbox.
This commit is contained in:
@@ -21,7 +21,17 @@ import {
|
||||
APIError,
|
||||
verifyTurnstileToken
|
||||
} from "~/server/fetch-utils";
|
||||
import { NETWORK_CONFIG, COOLDOWN_TIMERS, VALIDATION_CONFIG, TURNSTILE_CONFIG } from "~/config";
|
||||
import {
|
||||
NETWORK_CONFIG,
|
||||
COOLDOWN_TIMERS,
|
||||
VALIDATION_CONFIG,
|
||||
TURNSTILE_CONFIG
|
||||
} from "~/config";
|
||||
import {
|
||||
CONTACT_RECIPIENT_EMAIL,
|
||||
CONTACT_SENDER,
|
||||
buildContactSubject
|
||||
} from "~/lib/contact-config";
|
||||
|
||||
// Allowed S3 key types — prevents path traversal via type parameter (p8-008)
|
||||
const ALLOWED_S3_TYPES = ["blog", "attachments", "avatars", "users"] as const;
|
||||
@@ -330,7 +340,14 @@ export const miscRouter = createTRPCRouter({
|
||||
.string()
|
||||
.min(1)
|
||||
.max(VALIDATION_CONFIG.MAX_CONTACT_MESSAGE_LENGTH),
|
||||
turnstileToken: z.string().min(1, "Please complete the security check")
|
||||
turnstileToken: z.string().min(1, "Please complete the security check"),
|
||||
/**
|
||||
* Per-site subject prefix injected into the outbound email subject
|
||||
* (task 09). Defaults to `"freno.me"` so existing callers (pre-task-09
|
||||
* main-site contact form) keep emitting the byte-identical legacy
|
||||
* subject `"freno.me Contact Request"`.
|
||||
*/
|
||||
subjectPrefix: z.string().min(1).max(50).optional().default("freno.me")
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
@@ -377,14 +394,12 @@ export const miscRouter = createTRPCRouter({
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
const subject = buildContactSubject(input.subjectPrefix);
|
||||
const sendinblueData = {
|
||||
sender: {
|
||||
name: "freno.me",
|
||||
email: "michael@freno.me"
|
||||
},
|
||||
to: [{ email: "michael@freno.me" }],
|
||||
htmlContent: `<html><head></head><body><div>Request Name: ${escapeHtml(input.name)}</div><div>Request Email: ${escapeHtml(input.email)}</div><div>Request Message: ${escapeHtml(input.message)}</div></body></html>`,
|
||||
subject: "freno.me Contact Request"
|
||||
sender: { ...CONTACT_SENDER },
|
||||
to: [{ email: CONTACT_RECIPIENT_EMAIL }],
|
||||
htmlContent: `<html><head></head><body><div>Source: ${escapeHtml(input.subjectPrefix)}</div><div>Request Name: ${escapeHtml(input.name)}</div><div>Request Email: ${escapeHtml(input.email)}</div><div>Request Message: ${escapeHtml(input.message)}</div></body></html>`,
|
||||
subject
|
||||
};
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user