"use client"; import { useCallback, useState, useEffect, useRef } 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 { testDiscordWebhook, updateForm } from "@/app/admin/form/actions"; import { Loader2, ChevronDown, Send } from "lucide-react"; import { getGuildRoles } from "@/app/actions/discord"; import type { DiscordRole } from "@/lib/discord/discord"; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area" import { getErrorMessage } from "@/lib/utils"; import { isValidDiscordWebhookUrl, previewWebhookTemplate, } from "@/lib/discord/discord-webhook-settings"; import { useDebouncedAutoSave, } from "@/hooks/use-debounced-autosave"; import { Badge } from "@/components/ui/badge"; import { SaveStatusIndicator } from "@/components/save-status-indicator"; interface FormExtraClientProps { form: { id: string; discordWebhookUrl: string | null; discordWebhookTemplate: string | null; discordWebhookUpdateTemplate: string | null; allowedRoles?: string[] | null; deniedRoles?: 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 [allowedRoles, setAllowedRoles] = useState(form.allowedRoles || []); const [deniedRoles, setDeniedRoles] = useState(form.deniedRoles || []); const [roles, setRoles] = useState([]); const [loadingRoles, setLoadingRoles] = useState(true); const [roleSearch, setRoleSearch] = useState(""); const [lastEdited, setLastEdited] = useState<"access" | "webhook" | null>(null); const [testingWebhook, setTestingWebhook] = useState(false); const valuesRef = useRef({ webhookUrl, webhookTemplate, webhookUpdateTemplate, allowedRoles, deniedRoles, }); useEffect(() => { getGuildRoles() .then(r => setRoles(r)) .catch(() => toast.error("Failed to load Discord roles.")) .finally(() => setLoadingRoles(false)); }, []); useEffect(() => { valuesRef.current = { webhookUrl, webhookTemplate, webhookUpdateTemplate, allowedRoles, deniedRoles, }; }, [ allowedRoles, deniedRoles, webhookTemplate, webhookUpdateTemplate, webhookUrl, ]); const saveSettings = useCallback(async () => { const values = valuesRef.current; await updateForm(form.id, { discordWebhookUrl: values.webhookUrl.trim() || null, discordWebhookTemplate: values.webhookTemplate.trim() || null, discordWebhookUpdateTemplate: values.webhookUpdateTemplate.trim() || null, allowedRoles: values.allowedRoles, deniedRoles: values.deniedRoles, }); router.refresh(); }, [form.id, router]); const autoSave = useDebouncedAutoSave({ save: saveSettings, delay: 1000 }); const markEdited = (section: "access" | "webhook") => { setLastEdited(section); autoSave.schedule(); }; const toggleRole = (kind: "allowed" | "denied", roleId: string) => { const list = kind === "allowed" ? allowedRoles : deniedRoles; const nextList = list.includes(roleId) ? list.filter((id) => id !== roleId) : [...list, roleId]; if (kind === "allowed") { const nextDenied = deniedRoles.filter((id) => id !== roleId); setAllowedRoles(nextList); setDeniedRoles(nextDenied); valuesRef.current = { ...valuesRef.current, allowedRoles: nextList, deniedRoles: nextDenied, }; } else { const nextAllowed = allowedRoles.filter((id) => id !== roleId); setDeniedRoles(nextList); setAllowedRoles(nextAllowed); valuesRef.current = { ...valuesRef.current, allowedRoles: nextAllowed, deniedRoles: nextList, }; } markEdited("access"); }; const renderRoleList = (kind: "allowed" | "denied", list: string[], label: string, desc: string) => { const selectedRoles = roles.filter(r => list.includes(r.id)); return (

{desc}

{loadingRoles ? (
Loading roles...
) : (
setRoleSearch(event.target.value)} onKeyDown={(event) => event.stopPropagation()} placeholder="Search roles" aria-label={`Search ${label.toLowerCase()}`} />
{roles.filter((role) => role.name.toLowerCase().includes(roleSearch.toLowerCase()) ).map(role => { const colorHex = role.color === 0 ? "#99aab5" : `#${role.color.toString(16).padStart(6, '0')}`; return ( toggleRole(kind, role.id)} onSelect={(e) => e.preventDefault()} >
{role.name}
); })} {roles.filter((role) => role.name.toLowerCase().includes(roleSearch.toLowerCase()) ).length === 0 && (
No roles found.
)}
)}
); }; const webhookUrlValid = !webhookUrl.trim() || isValidDiscordWebhookUrl(webhookUrl.trim()); const insertVariable = ( variable: string, target: "new" | "update" ) => { if (target === "new") { const next = `${webhookTemplate}${webhookTemplate ? " " : ""}{${variable}}`; setWebhookTemplate(next); valuesRef.current = { ...valuesRef.current, webhookTemplate: next }; } else { const next = `${webhookUpdateTemplate}${webhookUpdateTemplate ? " " : ""}{${variable}}`; setWebhookUpdateTemplate(next); valuesRef.current = { ...valuesRef.current, webhookUpdateTemplate: next }; } markEdited("webhook"); }; const updateWebhookValue = ( field: "webhookUrl" | "webhookTemplate" | "webhookUpdateTemplate", value: string ) => { valuesRef.current = { ...valuesRef.current, [field]: value }; if (field === "webhookUrl") setWebhookUrl(value); if (field === "webhookTemplate") setWebhookTemplate(value); if (field === "webhookUpdateTemplate") setWebhookUpdateTemplate(value); markEdited("webhook"); }; const handleTestWebhook = async () => { setTestingWebhook(true); try { await testDiscordWebhook( webhookUrl, previewWebhookTemplate(webhookTemplate) ); toast.success("Test message sent."); } catch (error) { toast.error(getErrorMessage(error, "Could not send the test message.")); } finally { setTestingWebhook(false); } }; return (
Access Control Restrict form access to specific Discord roles.
{renderRoleList( "allowed", allowedRoles, "Allowed Roles", "Users must have at least one of these roles to access the form. Leave empty to allow all." )} {renderRoleList( "denied", deniedRoles, "Denied Roles", "Users with any of these roles will be blocked from accessing the form, even if they have an allowed role." )}
Discord Webhook Send notifications to a Discord channel when a new submission is received. Leave URL empty to disable.
updateWebhookValue("webhookUrl", e.target.value)} placeholder="https://discord.com/api/webhooks/..." aria-invalid={!webhookUrlValid} /> {!webhookUrlValid && (

Enter a valid Discord webhook URL.

)}