From 1be1932814b9673fc134591844fb89e7f34f5dfc Mon Sep 17 00:00:00 2001 From: gunshiz Date: Mon, 27 Jul 2026 19:16:56 +0700 Subject: [PATCH] style: move button --- Dockerfile | 10 ++++-- app/actions/submissions.ts | 46 +++++++++++-------------- app/admin/form/[id]/client.tsx | 27 +++++++++------ lib/submission-webhook.test.ts | 63 ++++++++++++++++++++++++++++++++++ lib/submission-webhook.ts | 50 +++++++++++++++++++++++++++ next.config.ts | 3 ++ 6 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 lib/submission-webhook.test.ts create mode 100644 lib/submission-webhook.ts diff --git a/Dockerfile b/Dockerfile index 8ec11da..896ecb5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,13 @@ +# syntax=docker/dockerfile:1 + # Stage 1: Install dependencies FROM oven/bun:alpine AS deps WORKDIR /app # Copy only package files to leverage Docker layer caching COPY package.json bun.lock ./ # Install dependencies -RUN bun install --frozen-lockfile +RUN --mount=type=cache,target=/root/.bun/install/cache \ + bun install --frozen-lockfile # Stage 2: Build the application FROM oven/bun:alpine AS builder @@ -12,7 +15,8 @@ WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Build Next.js -RUN bun run build +RUN --mount=type=cache,target=/app/.next/cache \ + bun run build # Stage 3: Production runner FROM oven/bun:alpine AS runner @@ -27,4 +31,4 @@ COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json EXPOSE 3000 -CMD ["bun", "start"] \ No newline at end of file +CMD ["bun", "start"] diff --git a/app/actions/submissions.ts b/app/actions/submissions.ts index ccd1dd7..9f52889 100644 --- a/app/actions/submissions.ts +++ b/app/actions/submissions.ts @@ -9,11 +9,8 @@ import { authOptions } from "@/lib/auth-options"; import { requireAdmin, requireDiscordId, requireFormAccess } from "@/lib/auth"; import { sse } from "@/lib/sse"; import type { FormAnswerValue } from "@/lib/form-answer-values"; -import { - answersByTemplateIndex, - normalizeSubmissionAnswers, - submissionUpdateText, -} from "@/lib/submission-intake"; +import { normalizeSubmissionAnswers } from "@/lib/submission-intake"; +import { submissionWebhookMessage } from "@/lib/submission-webhook"; export async function submitForm( formId: string, @@ -132,30 +129,19 @@ export async function submitForm( ? form.discordWebhookUpdateTemplate : form.discordWebhookTemplate; - if (form.discordWebhookUrl && templateToUse) { + if (form.discordWebhookUrl) { try { - const answersDict = answersByTemplateIndex(questionsList, normalizedAnswers); - const updatesText = result.isUpdate - ? submissionUpdateText(questionsList, normalizedAnswers, result.oldAnswersDict) - : ""; - - let message = templateToUse; - // Replace {1}, {2}, etc. with value, and new variables - message = message.replace(/\{([^}]+)\}/g, (match: string, p1: string) => { - const key = p1.toLowerCase(); - if (key === "username" || key === "displayname") { - return session.user?.name || "Unknown User"; - } - if (key === "id") { - return discordId || "Unknown ID"; - } - if (key === "update") { - return updatesText; - } - return answersDict[p1] || "Not Provided"; + const message = submissionWebhookMessage({ + template: templateToUse, + isUpdate: result.isUpdate, + userName: session.user.name, + discordId, + questions: questionsList, + answers: normalizedAnswers, + oldAnswersByQuestionId: result.oldAnswersDict, }); - await fetch(form.discordWebhookUrl, { + const response = await fetch(form.discordWebhookUrl, { method: "POST", headers: { "Content-Type": "application/json", @@ -164,6 +150,14 @@ export async function submitForm( content: message, }), }); + if (!response.ok) { + const responseBody = await response.text(); + console.error("Discord webhook rejected submission notification", { + status: response.status, + statusText: response.statusText, + response: responseBody.slice(0, 500), + }); + } } catch (e) { console.error("Failed to send webhook", e); } diff --git a/app/admin/form/[id]/client.tsx b/app/admin/form/[id]/client.tsx index 943b9cd..300b481 100644 --- a/app/admin/form/[id]/client.tsx +++ b/app/admin/form/[id]/client.tsx @@ -473,7 +473,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC -
+
{q.imageUrl ? (
)} +
+ + handleUpdateQuestion(q.id, { required: checked }) + } + /> + +
{(q.type === "radio" || q.type === "checkbox") && ( @@ -557,16 +572,6 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
-
- handleUpdateQuestion(q.id, { required: checked })} - /> - -
{(q.type === "radio" || q.type === "checkbox") && (
{ + expect(submissionWebhookMessage({ + template: "New answer: {1} by {username} ({id})", + isUpdate: false, + userName: "Gunshiz", + discordId: "123", + questions, + answers, + oldAnswersByQuestionId: {}, + })).toBe("New answer: Erika by Gunshiz (123)"); +}); + +describe("default templates", () => { + test("sends a new-submission message when its template is blank", () => { + expect(submissionWebhookMessage({ + template: null, + isUpdate: false, + userName: "Gunshiz", + discordId: "123", + questions, + answers, + oldAnswersByQuestionId: {}, + })).toBe("New submission from Gunshiz (123)."); + }); + + test("sends an update message when its template is blank", () => { + expect(submissionWebhookMessage({ + template: " ", + isUpdate: true, + userName: "Gunshiz", + discordId: "123", + questions, + answers, + oldAnswersByQuestionId: { name: "Old name" }, + })).toContain("Submission updated by Gunshiz (123)."); + }); +}); diff --git a/lib/submission-webhook.ts b/lib/submission-webhook.ts new file mode 100644 index 0000000..1d28e81 --- /dev/null +++ b/lib/submission-webhook.ts @@ -0,0 +1,50 @@ +import { + answersByTemplateIndex, + submissionUpdateText, + type NormalizedSubmissionAnswer, + type SubmissionQuestion, +} from "@/lib/submission-intake"; + +const NEW_SUBMISSION_TEMPLATE = "New submission from {username} ({id})."; +const UPDATED_SUBMISSION_TEMPLATE = "Submission updated by {username} ({id}).\n{update}"; + +type SubmissionWebhookMessageOptions = { + template: string | null; + isUpdate: boolean; + userName: string | null | undefined; + discordId: string; + questions: SubmissionQuestion[]; + answers: NormalizedSubmissionAnswer[]; + oldAnswersByQuestionId: Record; +}; + +export function submissionWebhookMessage({ + template, + isUpdate, + userName, + discordId, + questions, + answers, + oldAnswersByQuestionId, +}: SubmissionWebhookMessageOptions) { + const answersByIndex = answersByTemplateIndex(questions, answers); + const updatesText = isUpdate + ? submissionUpdateText(questions, answers, oldAnswersByQuestionId) + : ""; + const messageTemplate = template?.trim() + || (isUpdate ? UPDATED_SUBMISSION_TEMPLATE : NEW_SUBMISSION_TEMPLATE); + + return messageTemplate.replace(/\{([^}]+)\}/g, (_match, variable: string) => { + const key = variable.toLowerCase(); + if (key === "username" || key === "displayname") { + return userName || "Unknown User"; + } + if (key === "id") { + return discordId; + } + if (key === "update") { + return updatesText; + } + return answersByIndex[variable] || "Not Provided"; + }); +} diff --git a/next.config.ts b/next.config.ts index a16b0b5..d1d22f8 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { serverExternalPackages: ["bun"], + experimental: { + turbopackFileSystemCacheForBuild: true, + }, turbopack: { root: process.cwd(), },