refactor: consolidate form client components and standardize layout modularity

This commit is contained in:
2026-05-28 17:56:03 +07:00 Unverified
parent 49e7690b7b
commit 9879fd8ee8
22 changed files with 844 additions and 288 deletions
+72
View File
@@ -0,0 +1,72 @@
"use client";
import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarHeader,
SidebarFooter,
} from "@/components/ui/sidebar";
import { LayoutDashboard, ClipboardList, ArrowLeft } from "lucide-react";
export function AdminSidebar() {
const pathname = usePathname();
return (
<Sidebar variant="sidebar" collapsible="icon">
<SidebarHeader className="border-b border-border py-4 px-4 flex items-center justify-between">
<span className="font-bold tracking-wider text-primary text-sm uppercase">
Erika Admin
</span>
</SidebarHeader>
<SidebarContent>
<SidebarGroup>
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === "/form/admin"}>
<Link href="/form/admin">
<LayoutDashboard className="w-4 h-4" />
<span>Admin Dashboard</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname.startsWith("/form/admin/result")}>
<Link href="/form/admin/result">
<ClipboardList className="w-4 h-4" />
<span>Result Page</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
</SidebarContent>
<SidebarFooter className="border-t border-border p-4">
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild>
<Link href="/form">
<ArrowLeft className="w-4 h-4" />
<span>Exit Admin</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarFooter>
</Sidebar>
);
}
+71
View File
@@ -0,0 +1,71 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Trash2, Loader2 } from "lucide-react";
import { deleteForm } from "@/app/actions/form";
import { toast } from "sonner";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
interface DeleteFormButtonProps {
formId: string;
}
export default function DeleteFormButton({ formId }: DeleteFormButtonProps) {
const [open, setOpen] = useState(false);
const [deleting, setDeleting] = useState(false);
const handleDelete = async () => {
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);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive hover:bg-destructive/10">
<Trash2 className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete the form, all of its questions, and all submissions/answers.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="ghost" disabled={deleting} onClick={() => setOpen(false)}>
Cancel
</Button>
<Button variant="destructive" disabled={deleting} onClick={handleDelete}>
{deleting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Deleting...
</>
) : (
"Delete"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+40
View File
@@ -0,0 +1,40 @@
"use client";
import { signIn } from "next-auth/react";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
export default function LoginDialog() {
return (
<Dialog open>
<DialogContent onInteractOutside={(e) => e.preventDefault()}>
<DialogHeader>
<DialogTitle className="text-xl font-bold">Login Required</DialogTitle>
<DialogDescription className="text-base mt-1">
You need to login to continue.
</DialogDescription>
</DialogHeader>
<Button
className="w-full gap-2 mt-2"
onClick={() => signIn("discord", { callbackUrl: window.location.href })}
>
<svg
className="w-5 h-5"
viewBox="0 0 127.14 96.36"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z" />
</svg>
Continue with Discord
</Button>
</DialogContent>
</Dialog>
);
}