Change alert dialog to dialog

This commit is contained in:
vergiLgood1 2025-03-30 22:41:47 +07:00
parent 9380c371f8
commit 99692c37da
5 changed files with 233 additions and 170 deletions

View File

@ -1,29 +1,26 @@
import { useState } from "react" import { useState, useEffect } from "react"
import { Loader2, ShieldAlert } from 'lucide-react' import { Loader2, ShieldAlert } from 'lucide-react'
import { import {
AlertDialog, Dialog,
AlertDialogAction, DialogContent,
AlertDialogCancel, DialogDescription,
AlertDialogContent, DialogFooter,
AlertDialogDescription, DialogHeader,
AlertDialogFooter, DialogTitle,
AlertDialogHeader, } from "@/app/_components/ui/dialog"
AlertDialogTitle,
} from "@/app/_components/ui/alert-dialog"
import { Button } from "@/app/_components/ui/button" import { Button } from "@/app/_components/ui/button"
import { RadioGroup, RadioGroupItem } from "@/app/_components/ui/radio-group"
import { Label } from "@/app/_components/ui/label" import { Label } from "@/app/_components/ui/label"
import { Input } from "@/app/_components/ui/input" import { Input } from "@/app/_components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/app/_components/ui/select"
import { RadioGroup, RadioGroupItem } from "@/app/_components/ui/radio-group"
import { ValidBanDuration } from "@/app/_lib/types/ban-duration" import { ValidBanDuration } from "@/app/_lib/types/ban-duration"
import { toast } from "sonner"
interface BanUserDialogProps { interface BanUserDialogProps {
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
onConfirm: (duration: ValidBanDuration) => void onConfirm: (duration: ValidBanDuration) => void
isPending?: boolean isPending?: boolean
userId: string
} }
type BanDurationType = "preset" | "custom" type BanDurationType = "preset" | "custom"
@ -33,35 +30,44 @@ export function BanUserDialog({
onOpenChange, onOpenChange,
onConfirm, onConfirm,
isPending = false, isPending = false,
userId,
}: BanUserDialogProps) { }: BanUserDialogProps) {
const [durationType, setDurationType] = useState<BanDurationType>("preset") const [durationType, setDurationType] = useState<BanDurationType>("preset")
const [presetDuration, setPresetDuration] = useState("24h") const [presetDuration, setPresetDuration] = useState("24h")
const [customValue, setCustomValue] = useState("1") const [customValue, setCustomValue] = useState("1")
const [customUnit, setCustomUnit] = useState("days") const [customUnit, setCustomUnit] = useState("days")
useEffect(() => {
if (!open) {
// Reset form when dialog closes
setDurationType("preset")
setPresetDuration("24h")
setCustomValue("1")
setCustomUnit("days")
}
}, [open])
const handleConfirm = () => { const handleConfirm = () => {
let duration = "" let duration = "24h"
if (durationType === "preset") { if (durationType === "preset") {
duration = presetDuration duration = presetDuration
} else { } else {
// Convert to hours for consistency const value = parseInt(customValue)
if (isNaN(value) || value < 1) return toast.error("Invalid duration")
switch (customUnit) { switch (customUnit) {
case "hours": case "hours":
duration = `${customValue}h` duration = `${value}h`
break break
case "days": case "days":
duration = `${parseInt(customValue) * 24}h` duration = `${value * 24}h`
break break
case "weeks": case "weeks":
duration = `${parseInt(customValue) * 24 * 7}h` duration = `${value * 24 * 7}h`
break break
case "months": case "months":
duration = `${parseInt(customValue) * 24 * 30}h` // Approximation duration = `${value * 24 * 30}h`
break break
default:
duration = `${customValue}h`
} }
} }
@ -69,29 +75,29 @@ export function BanUserDialog({
} }
return ( return (
<AlertDialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent> <DialogContent className="sm:max-w-md border-0">
<AlertDialogHeader> <DialogHeader>
<AlertDialogTitle>Ban User</AlertDialogTitle> <DialogTitle>Ban User</DialogTitle>
<AlertDialogDescription> <DialogDescription>
This will prevent the user from accessing the system until the ban expires. This will prevent the user from accessing the system until the ban expires.
</AlertDialogDescription> </DialogDescription>
</AlertDialogHeader> </DialogHeader>
<div className="py-4"> <div className="py-4 space-y-4">
<RadioGroup <RadioGroup
value={durationType} value={durationType}
onValueChange={(value) => setDurationType(value as BanDurationType)} onValueChange={(v) => setDurationType(v as BanDurationType)}
className="space-y-4" className="space-y-4"
> >
<div className="flex items-start space-x-2"> <div className="flex items-start space-x-2">
<RadioGroupItem value="preset" id="preset" /> <RadioGroupItem value="preset" id="preset" disabled={isPending} />
<div className="grid gap-2.5 w-full"> <div className="grid gap-2.5 w-full">
<Label htmlFor="preset" className="font-medium">Use preset duration</Label> <Label htmlFor="preset">Use preset duration</Label>
<Select <Select
value={presetDuration} value={presetDuration}
onValueChange={setPresetDuration} onValueChange={setPresetDuration}
disabled={durationType !== "preset"} disabled={durationType !== "preset" || isPending}
> >
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="Select duration" /> <SelectValue placeholder="Select duration" />
@ -110,22 +116,22 @@ export function BanUserDialog({
</div> </div>
<div className="flex items-start space-x-2"> <div className="flex items-start space-x-2">
<RadioGroupItem value="custom" id="custom" /> <RadioGroupItem value="custom" id="custom" disabled={isPending} />
<div className="grid gap-2.5 w-full"> <div className="grid gap-2.5 w-full">
<Label htmlFor="custom" className="font-medium">Custom duration</Label> <Label htmlFor="custom">Custom duration</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
type="number" type="number"
min="1" min="1"
value={customValue} value={customValue}
onChange={(e) => setCustomValue(e.target.value)} onChange={(e) => setCustomValue(e.target.value)}
disabled={durationType !== "custom"} disabled={durationType !== "custom" || isPending}
className="w-20" className="w-20"
/> />
<Select <Select
value={customUnit} value={customUnit}
onValueChange={setCustomUnit} onValueChange={setCustomUnit}
disabled={durationType !== "custom"} disabled={durationType !== "custom" || isPending}
> >
<SelectTrigger> <SelectTrigger>
<SelectValue /> <SelectValue />
@ -143,27 +149,35 @@ export function BanUserDialog({
</RadioGroup> </RadioGroup>
</div> </div>
<AlertDialogFooter> <DialogFooter className="gap-2 sm:gap-0">
<AlertDialogCancel>Cancel</AlertDialogCancel> <Button
<AlertDialogAction variant="outline"
onClick={handleConfirm} onClick={() => onOpenChange(false)}
className="bg-yellow-500 text-white hover:bg-yellow-600"
disabled={isPending} disabled={isPending}
>
Cancel
</Button>
<Button
className="bg-yellow-500 text-white hover:bg-yellow-600"
onClick={handleConfirm}
disabled={isPending}
type="submit"
> >
{isPending ? ( {isPending ? (
<> <>
<Loader2 className="h-4 w-4 mr-2 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
Banning... Banning...
</> </>
) : ( ) : (
<> <>
<ShieldAlert className="h-4 w-4 mr-2" /> <ShieldAlert className="h-4 w-4" />
Ban User Ban User
</> </>
)} )}
</AlertDialogAction> </Button>
</AlertDialogFooter> </DialogFooter>
</AlertDialogContent> </DialogContent>
</AlertDialog> </Dialog>
) )
} }

View File

@ -11,17 +11,6 @@ import {
import { Button } from "@/app/_components/ui/button"; import { Button } from "@/app/_components/ui/button";
import { Badge } from "@/app/_components/ui/badge"; import { Badge } from "@/app/_components/ui/badge";
import { Separator } from "@/app/_components/ui/separator"; import { Separator } from "@/app/_components/ui/separator";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/app/_components/ui/alert-dialog";
import { import {
Mail, Mail,
Trash2, Trash2,

View File

@ -1,4 +1,5 @@
"use client" "use client"
import type { ColumnDef, HeaderContext } from "@tanstack/react-table" import type { ColumnDef, HeaderContext } from "@tanstack/react-table"
import type { IUserSchema, IUserFilterOptionsSchema } from "@/src/entities/models/users/users.model" import type { IUserSchema, IUserFilterOptionsSchema } from "@/src/entities/models/users/users.model"
import { ListFilter, MoreHorizontal, PenIcon as UserPen, Trash2, ShieldAlert, ShieldCheck } from "lucide-react" import { ListFilter, MoreHorizontal, PenIcon as UserPen, Trash2, ShieldAlert, ShieldCheck } from "lucide-react"
@ -15,7 +16,7 @@ import { Input } from "@/app/_components/ui/input"
import { Avatar } from "@/app/_components/ui/avatar" import { Avatar } from "@/app/_components/ui/avatar"
import Image from "next/image" import Image from "next/image"
import { Badge } from "@/app/_components/ui/badge" import { Badge } from "@/app/_components/ui/badge"
import { CAlertDialog } from "@/app/_components/alert-dialog" import { ConfirmDialog } from "@/app/_components/confirm-dialog"
import { useCreateUserColumn } from "../_handlers/use-create-user-column" import { useCreateUserColumn } from "../_handlers/use-create-user-column"
import { BanUserDialog } from "./ban-user-dialog" import { BanUserDialog } from "./ban-user-dialog"
import { ValidBanDuration } from "@/app/_lib/types/ban-duration" import { ValidBanDuration } from "@/app/_lib/types/ban-duration"
@ -31,22 +32,22 @@ export const createUserColumns = (
const { const {
deleteDialogOpen, deleteDialogOpen,
setDeleteDialogOpen, setDeleteDialogOpen,
userToDelete,
setUserToDelete,
handleDeleteConfirm, handleDeleteConfirm,
isDeletePending, isDeletePending,
banDialogOpen, banDialogOpen,
setBanDialogOpen, setBanDialogOpen,
userToBan,
setUserToBan,
handleBanConfirm, handleBanConfirm,
unbanDialogOpen, unbanDialogOpen,
setUnbanDialogOpen, setUnbanDialogOpen,
userToUnban,
setUserToUnban,
isBanPending, isBanPending,
isUnbanPending, isUnbanPending,
handleUnbanConfirm, handleUnbanConfirm,
selectedUser,
setSelectedUser,
} = useCreateUserColumn() } = useCreateUserColumn()
return [ return [
@ -339,7 +340,7 @@ export const createUserColumns = (
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setUserToDelete(row.original.id) setSelectedUser({ id: row.original.id, email: row.original.email! })
setDeleteDialogOpen(true) setDeleteDialogOpen(true)
}} }}
> >
@ -349,10 +350,10 @@ export const createUserColumns = (
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
if (row.original.banned_until != null) { if (row.original.banned_until != null) {
setUserToUnban(row.original.id) setSelectedUser({ id: row.original.id, email: row.original.email! })
setUnbanDialogOpen(true) setUnbanDialogOpen(true)
} else { } else {
setUserToBan(row.original.id) setSelectedUser({ id: row.original.id, email: row.original.email! })
setBanDialogOpen(true) setBanDialogOpen(true)
} }
}} }}
@ -364,48 +365,42 @@ export const createUserColumns = (
</DropdownMenu> </DropdownMenu>
{/* Alert Dialog for Delete Confirmation */} {/* Alert Dialog for Delete Confirmation */}
{deleteDialogOpen && userToDelete === row.original.id && ( <ConfirmDialog
<CAlertDialog
open={deleteDialogOpen} open={deleteDialogOpen}
onOpenChange={setDeleteDialogOpen} onOpenChange={setDeleteDialogOpen}
title="Are you absolutely sure?" title="Are you absolutely sure?"
description="This action cannot be undone. This will permanently delete the user account and remove their data from our servers." description="This action cannot be undone. This will permanently delete the user account and remove their data from our servers."
confirmText="Delete" confirmText="Delete"
onConfirm={handleDeleteConfirm} onConfirm={() => handleDeleteConfirm(row.original.id, row.original.email!)}
isPending={isDeletePending} isPending={isDeletePending}
pendingText="Deleting..." pendingText="Deleting..."
variant="destructive" variant="destructive"
size="sm" size="sm"
/> />
)}
{/* Alert Dialog for Ban Confirmation */} {/* Alert Dialog for Ban Confirmation */}
{banDialogOpen && userToBan === row.original.id && (
<BanUserDialog <BanUserDialog
open={banDialogOpen} open={banDialogOpen}
onOpenChange={setBanDialogOpen} onOpenChange={setBanDialogOpen}
onConfirm={(duration: ValidBanDuration) => handleBanConfirm(duration)} onConfirm={handleBanConfirm}
isPending={isBanPending} isPending={isBanPending}
userId={row.original.id}
/> />
)}
{/* Alert Dialog for Unban Confirmation */} {/* Alert Dialog for Unban Confirmation */}
{unbanDialogOpen && userToUnban === row.original.id && ( <ConfirmDialog
<CAlertDialog
open={unbanDialogOpen} open={unbanDialogOpen}
onOpenChange={setUnbanDialogOpen} onOpenChange={setUnbanDialogOpen}
title="Unban User" title="Unban User"
description="This will restore the user's access to the system. Are you sure you want to unban this user?" description="This will restore the user's access to the system. Are you sure you want to unban this user?"
confirmText="Unban" confirmText="Unban"
onConfirm={handleUnbanConfirm} onConfirm={() => handleUnbanConfirm(row.original.id, row.original.email!)}
isPending={isUnbanPending} isPending={isUnbanPending}
pendingText="Unbanning..." pendingText="Unbanning..."
variant="outline" variant="default"
size="sm" size="sm"
triggerIcon={<ShieldCheck className="h-4 w-4" />} confirmIcon={<ShieldCheck className="h-4 w-4" />}
/> />
)}
</div> </div>
), ),
}, },

View File

@ -3,6 +3,9 @@ import { useBanUserMutation, useDeleteUserMutation, useUnbanUserMutation } from
import { ValidBanDuration } from "@/app/_lib/types/ban-duration" import { ValidBanDuration } from "@/app/_lib/types/ban-duration"
import { useQueryClient } from "@tanstack/react-query" import { useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner" import { toast } from "sonner"
import { useForm } from "react-hook-form"
export const useCreateUserColumn = () => { export const useCreateUserColumn = () => {
@ -10,111 +13,99 @@ export const useCreateUserColumn = () => {
// Delete user state and handlers // Delete user state and handlers
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
const [userToDelete, setUserToDelete] = useState<string | null>(null)
const { mutateAsync: deleteUser, isPending: isDeletePending } = useDeleteUserMutation() const { mutateAsync: deleteUser, isPending: isDeletePending } = useDeleteUserMutation()
// Ban user state and handlers // Ban user state and handlers
const [banDialogOpen, setBanDialogOpen] = useState(false) const [banDialogOpen, setBanDialogOpen] = useState(false)
const [userToBan, setUserToBan] = useState<string | null>(null)
const { mutateAsync: banUser, isPending: isBanPending } = useBanUserMutation() const { mutateAsync: banUser, isPending: isBanPending } = useBanUserMutation()
// Unban user state and handlers // Unban user state and handlers
const [unbanDialogOpen, setUnbanDialogOpen] = useState(false) const [unbanDialogOpen, setUnbanDialogOpen] = useState(false)
const [userToUnban, setUserToUnban] = useState<string | null>(null)
const { mutateAsync: unbanUser, isPending: isUnbanPending } = useUnbanUserMutation() const { mutateAsync: unbanUser, isPending: isUnbanPending } = useUnbanUserMutation()
const handleDeleteConfirm = async () => { // Store selected user info
if (userToDelete) { const [selectedUser, setSelectedUser] = useState<{ id: string, email: string } | null>(null)
await deleteUser(userToDelete, {
const handleDeleteConfirm = async (userId: string, email: string) => {
if (!userId) return toast.error("No user selected to delete")
await deleteUser(userId, {
onSuccess: () => { onSuccess: () => {
if (isDeletePending) {
queryClient.invalidateQueries({ queryKey: ["users"] }) queryClient.invalidateQueries({ queryKey: ["users"] })
toast.success(`${userToDelete} has been deleted`) toast.success(`${email} has been deleted`)
setDeleteDialogOpen(false) setDeleteDialogOpen(false)
setUserToDelete(null)
}
}, },
onError: (error) => { onError: (error) => {
toast.error("Failed to delete user. Please try again later.") toast.error("Failed to delete user. Please try again later.")
setDeleteDialogOpen(false) setDeleteDialogOpen(false)
setUserToDelete(null)
} }
}) })
}
} }
const handleBanConfirm = async (duration: ValidBanDuration) => { const handleBanConfirm = async (duration: ValidBanDuration) => {
if (userToBan) {
await banUser({ id: userToBan, ban_duration: duration }, { if (!selectedUser) return toast.error("No user selected to ban")
await banUser({ id: selectedUser.id, ban_duration: duration }, {
onSuccess: () => { onSuccess: () => {
if (!isBanPending) { toast(`${selectedUser.email} has been banned`)
queryClient.invalidateQueries({ queryKey: ["users"] }) queryClient.invalidateQueries({ queryKey: ["users"] })
toast(`${userToBan} has been banned`)
setBanDialogOpen(false) setBanDialogOpen(false)
setUserToBan(null) setSelectedUser(null)
}
}, },
onError: (error) => { onError: () => {
toast.error("Failed to ban user. Please try again later.") toast.error("Failed to ban user. Please try again later.")
setBanDialogOpen(false) setBanDialogOpen(false)
setUserToBan(null) setSelectedUser(null)
}, },
}) })
} }
}
const handleUnbanConfirm = async () => { const handleUnbanConfirm = async (userId: string, email: string) => {
if (userToUnban) {
await unbanUser({ id: userToUnban }, { if (!userId) return toast.error("No user selected to unban")
await unbanUser({ id: userId }, {
onSuccess: () => { onSuccess: () => {
if (!isUnbanPending) {
queryClient.invalidateQueries({ queryKey: ["users"] }) queryClient.invalidateQueries({ queryKey: ["users"] })
toast(`${userToUnban} has been unbanned`) toast(`${email} has been unbanned`)
setUnbanDialogOpen(false) setUnbanDialogOpen(false)
setUserToUnban(null)
}
}, },
onError: (error) => { onError: (error) => {
toast.error("Failed to unban user. Please try again later.") toast.error("Failed to unban user. Please try again later.")
setUnbanDialogOpen(false) setUnbanDialogOpen(false)
setUserToUnban(null)
} }
}) })
} }
}
return { return {
// Delete // Delete
deleteDialogOpen, deleteDialogOpen,
setDeleteDialogOpen, setDeleteDialogOpen,
userToDelete,
setUserToDelete,
handleDeleteConfirm, handleDeleteConfirm,
isDeletePending, isDeletePending,
// Ban // Ban
banDialogOpen, banDialogOpen,
setBanDialogOpen, setBanDialogOpen,
userToBan,
setUserToBan,
handleBanConfirm, handleBanConfirm,
isBanPending, isBanPending,
// Unban // Unban
unbanDialogOpen, unbanDialogOpen,
setUnbanDialogOpen, setUnbanDialogOpen,
userToUnban,
setUserToUnban,
handleUnbanConfirm, handleUnbanConfirm,
isUnbanPending, isUnbanPending,
// Selected user
selectedUser,
setSelectedUser,
} }
} }

View File

@ -0,0 +1,74 @@
import type React from "react"
import { useState, useEffect } from "react"
import { Loader2 } from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/app/_components/ui/dialog"
import { Button, type ButtonProps } from "@/app/_components/ui/button"
interface ConfirmDialogProps {
title: string
description: string
confirmText?: string
cancelText?: string
onConfirm: () => void
isPending?: boolean
pendingText?: string
variant?: ButtonProps["variant"]
size?: ButtonProps["size"]
open?: boolean
onOpenChange?: (open: boolean) => void
confirmIcon?: React.ReactNode
}
export function ConfirmDialog({
title,
description,
confirmText = "Confirm",
cancelText = "Cancel",
onConfirm,
isPending = false,
pendingText = "Processing...",
variant = "default",
size = "default",
open,
onOpenChange,
confirmIcon,
}: ConfirmDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md border-0">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4 gap-2 sm:gap-0">
<Button variant="outline" onClick={() => onOpenChange?.(false)} disabled={isPending}>
{cancelText}
</Button>
<Button type="submit" variant={variant} onClick={onConfirm} disabled={isPending}>
{isPending ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
{pendingText}
</>
) : (
<>
{confirmIcon && <span className="">{confirmIcon}</span>}
{confirmText}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}