style: move button
This commit is contained in:
+7
-3
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).");
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,9 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
serverExternalPackages: ["bun"],
|
||||
experimental: {
|
||||
turbopackFileSystemCacheForBuild: true,
|
||||
},
|
||||
turbopack: {
|
||||
root: process.cwd(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user