From 752f708151a50a074629a7964bbee89924fb6594 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Fri, 3 Jul 2026 23:26:08 +0700 Subject: [PATCH] feat: implement debounced autosave functionality for form extra settings with status indicators --- app/admin/form/[id]/extra/client.tsx | 100 ++++++++++++++++++++------- 1 file changed, 74 insertions(+), 26 deletions(-) diff --git a/app/admin/form/[id]/extra/client.tsx b/app/admin/form/[id]/extra/client.tsx index 7f42d9f..0cfcfbe 100644 --- a/app/admin/form/[id]/extra/client.tsx +++ b/app/admin/form/[id]/extra/client.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useTransition, useEffect } from "react"; +import { useState, useTransition, 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"; @@ -9,7 +9,7 @@ import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { toast } from "sonner"; import { updateForm } from "@/app/admin/form/actions"; -import { Loader2, Save, ChevronDown } from "lucide-react"; +import { Loader2, ChevronDown, Check } from "lucide-react"; import { getGuildRoles, DiscordRole } from "@/app/actions/discord"; import { DropdownMenu, @@ -17,7 +17,7 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area" +import { ScrollArea } from "@/components/ui/scroll-area" interface FormExtraClientProps { form: { @@ -30,6 +30,8 @@ interface FormExtraClientProps { }; } +type AutoSaveStatus = "idle" | "saving" | "saved"; + export default function FormExtraClient({ form }: FormExtraClientProps) { const router = useRouter(); const [webhookUrl, setWebhookUrl] = useState(form.discordWebhookUrl || ""); @@ -42,6 +44,11 @@ export default function FormExtraClient({ form }: FormExtraClientProps) { const [loadingRoles, setLoadingRoles] = useState(true); const [isSaving, startSaveTransition] = useTransition(); + const [saveStatus, setSaveStatus] = useState("idle"); + const [lastEdited, setLastEdited] = useState<"access" | "webhook" | null>(null); + + const isFirstRender = useRef(true); + const saveTimeout = useRef(null); useEffect(() => { getGuildRoles() @@ -50,7 +57,8 @@ export default function FormExtraClient({ form }: FormExtraClientProps) { .finally(() => setLoadingRoles(false)); }, []); - const handleSave = () => { + const handleSave = (silent = false) => { + setSaveStatus("saving"); startSaveTransition(async () => { try { await updateForm(form.id, { @@ -60,15 +68,35 @@ export default function FormExtraClient({ form }: FormExtraClientProps) { allowedRoles, deniedRoles, }); - toast.success("Extra settings saved!"); + setSaveStatus("saved"); + setTimeout(() => setSaveStatus("idle"), 2000); router.refresh(); } catch (err: any) { + setSaveStatus("idle"); toast.error(err.message || "Failed to update settings."); } }); }; + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + + if (saveTimeout.current) clearTimeout(saveTimeout.current); + + saveTimeout.current = setTimeout(() => { + handleSave(true); + }, 1000); + + return () => { + if (saveTimeout.current) clearTimeout(saveTimeout.current); + }; + }, [webhookUrl, webhookTemplate, webhookUpdateTemplate, allowedRoles, deniedRoles]); + const toggleRole = (list: string[], setList: (v: string[]) => void, roleId: string) => { + setLastEdited("access"); if (list.includes(roleId)) { setList(list.filter(id => id !== roleId)); } else { @@ -151,11 +179,14 @@ export default function FormExtraClient({ form }: FormExtraClientProps) { return (
- - Access Control - - Restrict form access to specific Discord roles. - + +
+ Access Control + + Restrict form access to specific Discord roles. + +
+
{renderRoleList( @@ -174,12 +205,15 @@ export default function FormExtraClient({ form }: FormExtraClientProps) {
- - Discord Webhook - - Send notifications to a Discord channel when a new submission is received. - Leave URL empty to disable. - + +
+ Discord Webhook + + Send notifications to a Discord channel when a new submission is received. + Leave URL empty to disable. + +
+
@@ -187,7 +221,7 @@ export default function FormExtraClient({ form }: FormExtraClientProps) { setWebhookUrl(e.target.value)} + onChange={(e) => { setLastEdited("webhook"); setWebhookUrl(e.target.value); }} placeholder="https://discord.com/api/webhooks/..." />
@@ -196,7 +230,7 @@ export default function FormExtraClient({ form }: FormExtraClientProps) {