style: move button

This commit is contained in:
2026-07-27 19:16:56 +07:00 Unverified
parent bc856402e9
commit 1be1932814
6 changed files with 159 additions and 40 deletions
+7 -3
View File
@@ -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"]
CMD ["bun", "start"]
+20 -26
View File
@@ -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);
}
+16 -11
View File
@@ -473,7 +473,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
</div>
</div>
<div className="space-y-2">
<div className="flex flex-wrap items-center gap-4">
{q.imageUrl ? (
<div className="relative max-w-full h-auto inline-block shrink-0 group">
<Image
@@ -517,6 +517,21 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
/>
</div>
)}
<div className="flex items-center gap-2">
<Switch
id={`required-${q.id}`}
checked={q.required}
onCheckedChange={(checked) =>
handleUpdateQuestion(q.id, { required: checked })
}
/>
<Label
htmlFor={`required-${q.id}`}
className="text-sm font-normal text-muted-foreground"
>
Required
</Label>
</div>
</div>
{(q.type === "radio" || q.type === "checkbox") && (
@@ -557,16 +572,6 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
<div className="flex justify-between items-center pt-2 border-t border-border">
<div className="flex items-center flex-wrap gap-6">
<div className="flex items-center gap-2">
<Switch
id={`required-${q.id}`}
checked={q.required}
onCheckedChange={(checked) => handleUpdateQuestion(q.id, { required: checked })}
/>
<Label htmlFor={`required-${q.id}`} className="text-sm font-normal text-muted-foreground">
Required
</Label>
</div>
{(q.type === "radio" || q.type === "checkbox") && (
<div className="flex items-center gap-2">
<Switch
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, test } from "bun:test";
import { submissionWebhookMessage } from "@/lib/submission-webhook";
import type {
NormalizedSubmissionAnswer,
SubmissionQuestion,
} from "@/lib/submission-intake";
const questions: SubmissionQuestion[] = [
{
id: "name",
type: "text",
label: "Character name",
required: true,
options: [],
allowOther: false,
},
];
const answers: NormalizedSubmissionAnswer[] = [
{
questionId: "name",
value: "Erika",
displayValue: "Erika",
},
];
test("uses the configured template", () => {
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).");
});
});
+50
View File
@@ -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<string, string>;
};
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";
});
}
+3
View File
@@ -2,6 +2,9 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["bun"],
experimental: {
turbopackFileSystemCacheForBuild: true,
},
turbopack: {
root: process.cwd(),
},