add confirmation dialog

This commit is contained in:
2024-11-22 22:23:00 -06:00
parent 13648edf55
commit 55447c6de4
3 changed files with 135 additions and 13 deletions

View File

@@ -3,6 +3,8 @@
import {} from "@radix-ui/react-dropdown-menu"; import {} from "@radix-ui/react-dropdown-menu";
import { Dot, MoreHorizontal } from "lucide-react"; import { Dot, MoreHorizontal } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { toast } from "sonner";
import { useDialogConfirmation } from "~/app/_components/macro/confirmation-dialog";
import { Button } from "~/app/_components/ui/button"; import { Button } from "~/app/_components/ui/button";
import { import {
DropdownMenu, DropdownMenu,
@@ -12,29 +14,49 @@ import {
} from "~/app/_components/ui/dropdown-menu"; } from "~/app/_components/ui/dropdown-menu";
import { api } from "~/trpc/react"; import { api } from "~/trpc/react";
export const ListButton = ({ id, name }: { id: number; name: string }) => { const newName = "test new name";
const newName = "test new name";
const { mutate: updateMutation } = api.list.update.useMutation(); export const ListButton = ({ id, name }: { id: number; name: string }) => {
const { mutate: deleteMutation } = api.list.delete.useMutation(); const dialogConfirmation = useDialogConfirmation();
const { mutate: updateMutation } = api.list.update.useMutation({
onSuccess: () => {
toast.success("List successfully renamed.");
},
});
const { mutate: deleteMutation } = api.list.delete.useMutation({
onSuccess: () => {
toast.success("List successfully deleted.");
},
});
const handleRename = () => updateMutation({ listId: id, name: newName }); const handleRename = () => updateMutation({ listId: id, name: newName });
const handleDelete = () => deleteMutation({ listId: id });
const handleDelete = async () => {
const didConfirm = await dialogConfirmation({});
if (didConfirm) {
deleteMutation({ listId: id });
}
};
return ( return (
<Button <Button
asChild
variant="ghost" variant="ghost"
className="w-full justify-start underline-offset-4 hover:underline" className="group w-full cursor-default justify-start"
> >
<Link href={`/list/${id}`}> <>
<Dot className="mr-2 h-4 w-4" /> <Dot className="mr-2 h-4 w-4" />
{name} <Link
href={`/list/${id}`}
className="w-full text-left underline-offset-4 hover:underline"
>
{name}
</Link>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
className="data-[state=open]:bg-muted ml-auto flex h-8 w-8 p-0" className="data-[state=open]:bg-muted flex h-8 w-8 p-0"
> >
<MoreHorizontal /> <MoreHorizontal />
<span className="sr-only">Open menu</span> <span className="sr-only">Open menu</span>
@@ -45,7 +67,7 @@ export const ListButton = ({ id, name }: { id: number; name: string }) => {
<DropdownMenuItem onClick={handleDelete}>Delete</DropdownMenuItem> <DropdownMenuItem onClick={handleDelete}>Delete</DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</Link> </>
</Button> </Button>
); );
}; };

View File

@@ -0,0 +1,93 @@
"use client";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "~/app/_components/ui/dialog";
import { Button } from "~/app/_components/ui/button";
import { createContext, useContext, useState } from "react";
type DialogContextType = (options: {
title?: string;
description?: string;
confirmText?: string;
cancelText?: string;
}) => Promise<boolean>;
const DialogContext = createContext<DialogContextType | null>(null);
export const useDialogConfirmation = () => {
const context = useContext(DialogContext);
if (!context) {
throw new Error("useDialogConfirmation must be used within DialogProvider");
}
return context;
};
export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
const [dialogState, setDialogState] = useState<{
isOpen: boolean;
options: {
title?: string;
description?: string;
confirmText?: string;
cancelText?: string;
};
resolve: ((value: boolean) => void) | null;
}>({
isOpen: false,
options: {},
resolve: null,
});
const dialogConfirmation: DialogContextType = (options) =>
new Promise((resolve) => {
setDialogState({
isOpen: true,
options,
resolve,
});
});
const handleConfirm = () => {
if (dialogState.resolve) dialogState.resolve(true);
setDialogState({ ...dialogState, isOpen: false });
};
const handleCancel = () => {
if (dialogState.resolve) dialogState.resolve(false);
setDialogState({ ...dialogState, isOpen: false });
};
return (
<DialogContext.Provider value={dialogConfirmation}>
{children}
<Dialog
open={dialogState.isOpen}
onOpenChange={(isOpen) => setDialogState({ ...dialogState, isOpen })}
>
<DialogContent>
<DialogHeader>
<DialogTitle>
{dialogState.options.title ?? "Are you sure?"}
</DialogTitle>
</DialogHeader>
<p>
{dialogState.options.description ?? "This action cannot be undone."}
</p>
<DialogFooter>
<Button variant="secondary" onClick={handleCancel}>
{dialogState.options.cancelText ?? "Cancel"}
</Button>
<Button variant="destructive" onClick={handleConfirm}>
{dialogState.options.confirmText ?? "Confirm"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DialogContext.Provider>
);
};

View File

@@ -5,7 +5,9 @@ import { type Metadata } from "next";
import { TRPCReactProvider } from "~/trpc/react"; import { TRPCReactProvider } from "~/trpc/react";
import { HydrateClient } from "~/trpc/server"; import { HydrateClient } from "~/trpc/server";
import { AuthProvider } from "~/app/_components/auth-provider"; import { AuthProvider } from "~/app/_components/providers/auth-provider";
import { DialogProvider } from "~/app/_components/macro/confirmation-dialog";
import { Toaster } from "~/app/_components/ui/sonner";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "ls", title: "ls",
@@ -26,7 +28,12 @@ export default function RootLayout({
<body> <body>
<TRPCReactProvider> <TRPCReactProvider>
<HydrateClient> <HydrateClient>
<AuthProvider>{children}</AuthProvider> <AuthProvider>
<DialogProvider>
{children}
<Toaster />
</DialogProvider>
</AuthProvider>
</HydrateClient> </HydrateClient>
</TRPCReactProvider> </TRPCReactProvider>
</body> </body>