refactor: migrate global toast notifications to Sonner and update admin UI navigation, layouts, and submission workflows
This commit is contained in:
+32
-17
@@ -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) => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-6 max-w-2xl mx-auto">
|
||||
<div className="space-y-6 w-full pb-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/form/admin/result">
|
||||
@@ -75,6 +75,25 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
|
||||
questions={questions}
|
||||
answers={answersMap}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between items-center mt-8 pt-6 border-t">
|
||||
{prevId ? (
|
||||
<Link href={`/form/admin/result/${prevId}`}>
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> Previous
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{nextId && (
|
||||
<Link href={`/form/admin/result/${nextId}`}>
|
||||
<Button variant="outline">
|
||||
Next <ArrowRight className="w-4 h-4 ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-6 max-w-4xl mx-auto">
|
||||
<div className="space-y-6 w-full">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Form Submissions</h1>
|
||||
@@ -39,7 +39,7 @@ export default async function ResultsListPage() {
|
||||
<CardHeader>
|
||||
<CardTitle>Submissions</CardTitle>
|
||||
<CardDescription>
|
||||
Total {submissionsList.length} responses collected. Click view to inspect individual answers.
|
||||
Total {submissionsList.length} responses collected. Click on a submission to inspect individual answers.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
@@ -58,23 +58,20 @@ export default async function ResultsListPage() {
|
||||
: sub.userDiscordId || "Anonymous";
|
||||
|
||||
return (
|
||||
<TableRow key={sub.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
<TableRow key={sub.id} className="cursor-pointer group">
|
||||
<TableCell className="p-0">
|
||||
<Link href={`/form/admin/result/${sub.id}`} className="flex items-center gap-2 p-4 w-full h-full font-medium">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{userLabel}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(sub.submittedAt).toLocaleString()}
|
||||
<TableCell className="p-0 text-muted-foreground">
|
||||
<Link href={`/form/admin/result/${sub.id}`} className="flex items-center p-4 w-full h-full">
|
||||
{new Date(sub.submittedAt).toLocaleString()}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<TableCell className="text-right p-4">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Link href={`/form/admin/result/${sub.id}`}>
|
||||
<Button size="sm" variant="outline" className="gap-1">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DeleteSubmissionButton id={sub.id} />
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
+5
-1
@@ -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")}
|
||||
>
|
||||
<body className={`${anuphan.className} ${geistSans.className}`}>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Toaster richColors closeButton position="bottom-right" theme="dark" />
|
||||
{children}
|
||||
</TooltipProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ export function AdminSidebar() {
|
||||
<SidebarMenuButton asChild isActive={pathname === "/form/admin"}>
|
||||
<Link href="/form/admin">
|
||||
<LayoutDashboard className="w-4 h-4" />
|
||||
<span>Admin Dashboard</span>
|
||||
<span>Dashboard</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@@ -46,7 +46,7 @@ export function AdminSidebar() {
|
||||
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/result")}>
|
||||
<Link href="/form/admin/result">
|
||||
<ClipboardList className="w-4 h-4" />
|
||||
<span>Result Page</span>
|
||||
<span>Response</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
@@ -70,7 +70,7 @@ export function AdminSidebar() {
|
||||
<SidebarMenuButton asChild>
|
||||
<Link href="/form">
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
<span>Exit Admin</span>
|
||||
<span>Go public</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user