From bcae6ece38d44e2408baa286c20e997ae6af2cfe Mon Sep 17 00:00:00 2001 From: Gunshiz Date: Thu, 28 May 2026 18:51:55 +0700 Subject: [PATCH] feat: add Discord webhook configuration support to forms and create dedicated admin settings page --- app/actions/form.ts | 2 +- app/actions/submissions.ts | 42 ++ app/form/admin/extra/client.tsx | 102 +++++ app/form/admin/extra/page.tsx | 34 ++ components/admin-sidebar.tsx | 11 +- db/schema.ts | 3 + drizzle/0003_flimsy_virginia_dare.sql | 2 + drizzle/0004_round_sebastian_shaw.sql | 1 + drizzle/meta/0003_snapshot.json | 527 +++++++++++++++++++++++++ drizzle/meta/0004_snapshot.json | 533 ++++++++++++++++++++++++++ drizzle/meta/_journal.json | 14 + 11 files changed, 1269 insertions(+), 2 deletions(-) create mode 100644 app/form/admin/extra/client.tsx create mode 100644 app/form/admin/extra/page.tsx create mode 100644 drizzle/0003_flimsy_virginia_dare.sql create mode 100644 drizzle/0004_round_sebastian_shaw.sql create mode 100644 drizzle/meta/0003_snapshot.json create mode 100644 drizzle/meta/0004_snapshot.json diff --git a/app/actions/form.ts b/app/actions/form.ts index 5264f17..ebf4841 100644 --- a/app/actions/form.ts +++ b/app/actions/form.ts @@ -32,7 +32,7 @@ export async function createForm(formData: FormData) { redirect(`/form/admin/${form.id}/edit`); } -export async function updateForm(id: string, data: { title: string; description?: string; isOpen?: boolean }) { +export async function updateForm(id: string, data: { title?: string; description?: string | null; isOpen?: boolean; discordWebhookUrl?: string | null; discordWebhookTemplate?: string | null; discordWebhookUpdateTemplate?: string | null }) { const session = await getServerSession(authOptions); const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "").split(",").map((s) => s.trim()).filter(Boolean); const discordId = (session?.user as { discordId?: string } | undefined)?.discordId; diff --git a/app/actions/submissions.ts b/app/actions/submissions.ts index f995bb7..05a1f00 100644 --- a/app/actions/submissions.ts +++ b/app/actions/submissions.ts @@ -29,6 +29,7 @@ export async function submitForm( }); let submissionId = ""; + const isUpdate = !!existingSubmission; if (existingSubmission) { submissionId = existingSubmission.id; @@ -89,6 +90,47 @@ export async function submitForm( ); } + // Send Discord Webhook + const templateToUse = isUpdate ? (form as any).discordWebhookUpdateTemplate : form.discordWebhookTemplate; + + if (form.discordWebhookUrl && templateToUse) { + try { + const questionsList = await db.query.questions.findMany({ + where: (q, { eq }) => eq(q.formId, formId) + }); + const questionMap = questionsList.reduce((acc, q) => { + acc[q.id] = q.label; + return acc; + }, {} as Record); + + const answersDict = answersList.reduce((acc, a) => { + const label = questionMap[a.questionId]; + if (label) { + acc[label] = a.value; + } + return acc; + }, {} as Record); + + let message = templateToUse; + // Replace {Label} with value + message = message.replace(/\{([^}]+)\}/g, (match: string, p1: string) => { + return answersDict[p1] || "Not Provided"; + }); + + await fetch(form.discordWebhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: message, + }), + }); + } catch (e) { + console.error("Failed to send webhook", e); + } + } + revalidatePath("/form/admin/result"); return { ok: true }; } diff --git a/app/form/admin/extra/client.tsx b/app/form/admin/extra/client.tsx new file mode 100644 index 0000000..7195bd5 --- /dev/null +++ b/app/form/admin/extra/client.tsx @@ -0,0 +1,102 @@ +"use client"; + +import React, { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { toast } from "sonner"; +import { updateForm } from "@/app/actions/form"; +import { Loader2, Save } from "lucide-react"; + +interface FormExtraClientProps { + form: { + id: string; + discordWebhookUrl: string | null; + discordWebhookTemplate: string | null; + discordWebhookUpdateTemplate: string | null; + }; +} + +export default function FormExtraClient({ form }: FormExtraClientProps) { + const router = useRouter(); + const [webhookUrl, setWebhookUrl] = useState(form.discordWebhookUrl || ""); + const [webhookTemplate, setWebhookTemplate] = useState(form.discordWebhookTemplate || ""); + const [webhookUpdateTemplate, setWebhookUpdateTemplate] = useState(form.discordWebhookUpdateTemplate || ""); + const [isSaving, startSaveTransition] = useTransition(); + + const handleSave = () => { + startSaveTransition(async () => { + try { + await updateForm(form.id, { + discordWebhookUrl: webhookUrl.trim() || null, + discordWebhookTemplate: webhookTemplate.trim() || null, + discordWebhookUpdateTemplate: webhookUpdateTemplate.trim() || null, + }); + toast.success("Extra settings saved!"); + router.refresh(); + } catch (err: any) { + toast.error(err.message || "Failed to update settings."); + } + }); + }; + + return ( +
+ + + Discord Webhook + + Send notifications to a Discord channel when a new submission is received. + Leave URL empty to disable. + + + +
+ + setWebhookUrl(e.target.value)} + placeholder="https://discord.com/api/webhooks/..." + /> +
+
+ +