feat(dialog): add confirmation dialog and state management for user actions
This commit is contained in:
parent
5f427a5032
commit
bb2093d14f
@ -9,6 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-alert-dialog": "^1.1.2",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
@ -25,7 +26,8 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import ConfirmationDialog from "@/dialogs/ConfirmationDialog";
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
@ -44,6 +45,7 @@ export default function DashboardLayout({
|
||||
</header>
|
||||
{children}
|
||||
</SidebarInset>
|
||||
<ConfirmationDialog />
|
||||
</SidebarProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
|
@ -4,10 +4,9 @@ import {
|
||||
Bell,
|
||||
LogIn,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
BadgeCheck,
|
||||
CreditCard,
|
||||
Settings,
|
||||
ChevronsUpDown,
|
||||
CircleUserRound,
|
||||
} from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
@ -25,7 +24,8 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useSession } from "next-auth/react";
|
||||
import useConfirmationStore from "@/store/confirmationStore";
|
||||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
|
||||
const UserAvatar = ({ src, alt }: { src: string; alt: string }) => (
|
||||
@ -46,6 +46,7 @@ export const NavUser = ({
|
||||
}) => {
|
||||
const { isMobile } = useSidebar();
|
||||
const { data: session } = useSession();
|
||||
const { openConfirmation } = useConfirmationStore();
|
||||
|
||||
const currentUser = useMemo(() => {
|
||||
if (session?.user) {
|
||||
@ -104,28 +105,37 @@ export const NavUser = ({
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
<CircleUserRound />
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<CreditCard />
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Bell />
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
<Settings />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
openConfirmation({
|
||||
title: "Leaving Already?",
|
||||
description: "Are you sure you want to say goodbye?",
|
||||
cancelLabel: "Stay",
|
||||
actionLabel: "Leave",
|
||||
onCancel: () => {},
|
||||
onAction: () => {
|
||||
signOut();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LogOut />
|
||||
Log out
|
||||
</DropdownMenuItem>
|
||||
@ -133,7 +143,7 @@ export const NavUser = ({
|
||||
</>
|
||||
) : (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => signIn()}>
|
||||
<LogIn />
|
||||
Log in
|
||||
</DropdownMenuItem>
|
||||
|
141
src/components/ui/alert-dialog.tsx
Normal file
141
src/components/ui/alert-dialog.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||
|
||||
const AlertDialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName =
|
||||
AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action
|
||||
ref={ref}
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"mt-2 sm:mt-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
}
|
48
src/dialogs/ConfirmationDialog.tsx
Normal file
48
src/dialogs/ConfirmationDialog.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import useConfirmationStore from "@/store/confirmationStore";
|
||||
|
||||
const ConfirmationDialog = () => {
|
||||
const {
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
cancelLabel,
|
||||
actionLabel,
|
||||
onCancel,
|
||||
onAction,
|
||||
closeConfirmation,
|
||||
} = useConfirmationStore();
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={closeConfirmation}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription>{description}</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel onClick={onCancel}>
|
||||
{cancelLabel}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={onAction}>
|
||||
{actionLabel}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationDialog;
|
64
src/store/confirmationStore.ts
Normal file
64
src/store/confirmationStore.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
interface ConfirmationState {
|
||||
open: boolean;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
cancelLabel: string | null;
|
||||
actionLabel: string | null;
|
||||
onCancel: () => void;
|
||||
onAction: () => void;
|
||||
}
|
||||
|
||||
interface ConfirmationActions {
|
||||
openConfirmation: (data: {
|
||||
title: string;
|
||||
description: string;
|
||||
cancelLabel: string;
|
||||
actionLabel: string;
|
||||
onCancel: () => void;
|
||||
onAction: () => void;
|
||||
}) => void;
|
||||
closeConfirmation: () => void;
|
||||
}
|
||||
|
||||
const useConfirmationStore = create<ConfirmationState & ConfirmationActions>(
|
||||
(set) => ({
|
||||
open: false,
|
||||
title: null,
|
||||
description: null,
|
||||
cancelLabel: null,
|
||||
actionLabel: null,
|
||||
onCancel: () => {},
|
||||
onAction: () => {},
|
||||
openConfirmation: (data) =>
|
||||
set(() => ({
|
||||
open: true,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
cancelLabel: data.cancelLabel,
|
||||
actionLabel: data.actionLabel,
|
||||
onCancel: data.onCancel,
|
||||
onAction: data.onAction,
|
||||
})),
|
||||
closeConfirmation: () => {
|
||||
set((state) => ({
|
||||
...state,
|
||||
open: false,
|
||||
}));
|
||||
setTimeout(() => {
|
||||
set((state) => ({
|
||||
...state,
|
||||
title: null,
|
||||
description: null,
|
||||
cancelLabel: null,
|
||||
actionLabel: null,
|
||||
onCancel: () => {},
|
||||
onAction: () => {},
|
||||
}));
|
||||
}, 100);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default useConfirmationStore;
|
Loading…
Reference in New Issue
Block a user