From bc78873ff8b7152d441d4c0649217c25e791c037 Mon Sep 17 00:00:00 2001 From: Gunshiz Date: Thu, 28 May 2026 20:18:45 +0700 Subject: [PATCH] refactor: migrate global toast notifications to Sonner and update admin UI navigation, layouts, and submission workflows --- app/form/admin/client.tsx | 49 +++++++++++++------- app/form/admin/layout.tsx | 1 - app/form/admin/result/[id]/page.tsx | 23 +++++++++- app/form/admin/result/page.tsx | 27 +++++------ app/layout.tsx | 6 ++- components/admin-sidebar.tsx | 6 +-- components/delete-buttons.tsx | 71 ++++++++++++++++------------- 7 files changed, 113 insertions(+), 70 deletions(-) diff --git a/app/form/admin/client.tsx b/app/form/admin/client.tsx index 3313ef7..a959d87 100644 --- a/app/form/admin/client.tsx +++ b/app/form/admin/client.tsx @@ -83,10 +83,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC displayOrder, options: [], }); - toast.success("Question added!"); router.refresh(); } catch (err: any) { - toast.error(err.message || "Failed to add question."); + console.error("Failed to add question:", err); } }; @@ -111,10 +110,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC const handleDeleteQuestion = async (id: string) => { try { await deleteQuestion(id, form.id); - toast.success("Question deleted!"); router.refresh(); } catch (err: any) { - toast.error(err.message || "Failed to delete question."); + console.error("Failed to delete question:", err); } }; @@ -146,22 +144,39 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC formData.append("file", file); setUploadingId(questionId); - try { - const imageUrl = await uploadImage(formData); - await handleUpdateQuestion(questionId, { imageUrl }); - toast.success("Image uploaded!"); - router.refresh(); - } catch (err: any) { - toast.error(err.message || "Failed to upload image."); - } finally { - setUploadingId(null); - } + + toast.promise( + uploadImage(formData).then(async (imageUrl) => { + await updateQuestion(questionId, form.id, { imageUrl }); + setQuestions((prev) => + prev.map((q) => (q.id === questionId ? { ...q, imageUrl } : q)) + ); + router.refresh(); + }).finally(() => { + setUploadingId(null); + }), + { + loading: "Uploading image...", + success: "Image uploaded!", + error: (err: any) => err.message || "Failed to upload image.", + } + ); }; const handleRemoveImage = async (questionId: string) => { - await handleUpdateQuestion(questionId, { imageUrl: null }); - toast.success("Image removed."); - router.refresh(); + toast.promise( + updateQuestion(questionId, form.id, { imageUrl: null }).then(() => { + setQuestions((prev) => + prev.map((q) => (q.id === questionId ? { ...q, imageUrl: null } : q)) + ); + router.refresh(); + }), + { + loading: "Removing image...", + success: "Image removed.", + error: (err: any) => err.message || "Failed to remove image.", + } + ); }; const handleAddOption = async (question: Question) => { diff --git a/app/form/admin/layout.tsx b/app/form/admin/layout.tsx index 8ff7dd5..98768f4 100644 --- a/app/form/admin/layout.tsx +++ b/app/form/admin/layout.tsx @@ -1,6 +1,5 @@ import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AdminSidebar } from "@/components/admin-sidebar"; -import { Toaster } from "@/components/ui/sonner"; import { getServerSession } from "next-auth"; import { authOptions } from "@/app/api/auth/[...nextauth]/route"; import { redirect } from "next/navigation"; diff --git a/app/form/admin/result/[id]/page.tsx b/app/form/admin/result/[id]/page.tsx index 5c98dee..b17d609 100644 --- a/app/form/admin/result/[id]/page.tsx +++ b/app/form/admin/result/[id]/page.tsx @@ -2,7 +2,7 @@ import { db } from "@/db"; import { notFound } from "next/navigation"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { ArrowLeft } from "lucide-react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; import SubmissionDetailClient from "./client"; import { DeleteSubmissionButton } from "@/components/delete-buttons"; @@ -45,7 +45,7 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP const nextId = currentIndex >= 0 && currentIndex < submissionsList.length - 1 ? submissionsList[currentIndex + 1].id : null; return ( -
+
@@ -75,6 +75,25 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP questions={questions} answers={answersMap} /> + +
+ {prevId ? ( + + + + ) : ( +
+ )} + {nextId && ( + + + + )} +
); } diff --git a/app/form/admin/result/page.tsx b/app/form/admin/result/page.tsx index 7204dde..c603bb8 100644 --- a/app/form/admin/result/page.tsx +++ b/app/form/admin/result/page.tsx @@ -3,7 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import Link from "next/link"; -import { Eye, Users } from "lucide-react"; +import { Users } from "lucide-react"; import { DeleteSubmissionButton, DeleteAllSubmissionsButton } from "@/components/delete-buttons"; export const dynamic = "force-dynamic"; @@ -14,7 +14,7 @@ export default async function ResultsListPage() { }); return ( -
+

Form Submissions

@@ -39,7 +39,7 @@ export default async function ResultsListPage() { Submissions - Total {submissionsList.length} responses collected. Click view to inspect individual answers. + Total {submissionsList.length} responses collected. Click on a submission to inspect individual answers. @@ -58,23 +58,20 @@ export default async function ResultsListPage() { : sub.userDiscordId || "Anonymous"; return ( - - -
+ + + {userLabel} -
+
- - {new Date(sub.submittedAt).toLocaleString()} + + + {new Date(sub.submittedAt).toLocaleString()} + - +
- - -
diff --git a/app/layout.tsx b/app/layout.tsx index ce22c56..24cb38d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -19,6 +19,7 @@ export const metadata: Metadata = { }; import { TooltipProvider } from "@/components/ui/tooltip"; +import { Toaster } from "sonner"; export default function RootLayout({ children, @@ -31,7 +32,10 @@ export default function RootLayout({ className={cn("h-full", "dark", "antialiased", anuphan.variable, geistSans.variable, "font-sans")} > - {children} + + + {children} + ); diff --git a/components/admin-sidebar.tsx b/components/admin-sidebar.tsx index 071ac7f..3c3792b 100644 --- a/components/admin-sidebar.tsx +++ b/components/admin-sidebar.tsx @@ -37,7 +37,7 @@ export function AdminSidebar() { - Admin Dashboard + Dashboard @@ -46,7 +46,7 @@ export function AdminSidebar() { - Result Page + Response @@ -70,7 +70,7 @@ export function AdminSidebar() { - Exit Admin + Go public diff --git a/components/delete-buttons.tsx b/components/delete-buttons.tsx index 077bec8..4d639ba 100644 --- a/components/delete-buttons.tsx +++ b/components/delete-buttons.tsx @@ -37,17 +37,20 @@ export function DeleteFormButton({ formId }: DeleteFormButtonProps) { const [open, setOpen] = useState(false); const [deleting, setDeleting] = useState(false); - const handleDelete = async () => { + const handleDelete = () => { setDeleting(true); - try { - await deleteForm(formId); - toast.success("Form deleted successfully!"); - setOpen(false); - } catch (err: any) { - toast.error(err.message || "Failed to delete form."); - } finally { - setDeleting(false); - } + toast.promise(deleteForm(formId), { + loading: "Deleting form...", + success: () => { + setOpen(false); + setDeleting(false); + return "Form deleted successfully!"; + }, + error: (err: any) => { + setDeleting(false); + return err.message || "Failed to delete form."; + }, + }); }; return ( @@ -88,19 +91,22 @@ export function DeleteSubmissionButton({ id, redirectUrl }: { id: string, redire const [loading, setLoading] = useState(false); const router = useRouter(); - const handleDelete = async () => { + const handleDelete = () => { setLoading(true); - try { - await deleteSubmission(id); - toast.success("Submission deleted"); - if (redirectUrl) { - router.push(redirectUrl); - } - } catch (err: any) { - toast.error(err.message || "Failed to delete submission"); - } finally { - setLoading(false); - } + toast.promise(deleteSubmission(id), { + loading: "Deleting submission...", + success: () => { + if (redirectUrl) { + router.push(redirectUrl); + } + setLoading(false); + return "Submission deleted"; + }, + error: (err: any) => { + setLoading(false); + return err.message || "Failed to delete submission"; + }, + }); }; return ( @@ -132,16 +138,19 @@ export function DeleteSubmissionButton({ id, redirectUrl }: { id: string, redire export function DeleteAllSubmissionsButton() { const [loading, setLoading] = useState(false); - const handleDeleteAll = async () => { + const handleDeleteAll = () => { setLoading(true); - try { - await deleteAllSubmissions(); - toast.success("All submissions deleted"); - } catch (err: any) { - toast.error(err.message || "Failed to delete submissions"); - } finally { - setLoading(false); - } + toast.promise(deleteAllSubmissions(), { + loading: "Deleting all submissions...", + success: () => { + setLoading(false); + return "All submissions deleted"; + }, + error: (err: any) => { + setLoading(false); + return err.message || "Failed to delete submissions"; + }, + }); }; return (