164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
"use client";
|
|
import { useIsMutating } from "@tanstack/react-query";
|
|
import React, { useEffect, useState } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { z } from "zod";
|
|
|
|
const ConfirmationModalPropsSchema = z.object({
|
|
isOpen: z.boolean(),
|
|
title: z.string().min(1, "Title is required"),
|
|
message: z.string().min(1, "Message is required"),
|
|
confirmText: z.string().optional().default("Confirm"),
|
|
cancelText: z.string().optional().default("Cancel"),
|
|
onConfirm: z.function(),
|
|
onCancel: z.function(),
|
|
variant: z
|
|
.enum(["default", "danger", "warning"])
|
|
.optional()
|
|
.default("default"),
|
|
size: z.enum(["sm", "md", "lg"]).optional().default("md"),
|
|
});
|
|
|
|
export type ConfirmationModalProps = z.infer<
|
|
typeof ConfirmationModalPropsSchema
|
|
>;
|
|
|
|
const ConfirmationModal: React.FC<ConfirmationModalProps> = (props) => {
|
|
const isMutating = useIsMutating();
|
|
const validatedProps = ConfirmationModalPropsSchema.parse(props);
|
|
const {
|
|
isOpen,
|
|
title,
|
|
message,
|
|
confirmText,
|
|
cancelText,
|
|
onConfirm,
|
|
onCancel,
|
|
variant,
|
|
size,
|
|
} = validatedProps;
|
|
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setMounted(true);
|
|
return () => setMounted(false);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key === "Escape" && isOpen) {
|
|
onCancel();
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document?.addEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "hidden";
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("keydown", handleEscape);
|
|
document.body.style.overflow = "unset";
|
|
};
|
|
}, [isOpen, onCancel]);
|
|
|
|
if (!mounted || !isOpen) return null;
|
|
|
|
const variantStyles = {
|
|
default: {
|
|
confirmBtn: "bg-primary hover:bg-primary/90 focus:ring-primary",
|
|
icon: "🔔",
|
|
iconBg: "bg-blue-100",
|
|
},
|
|
danger: {
|
|
confirmBtn: "bg-red-light hover:bg-red-light/90 focus:ring-red-light",
|
|
icon: "⚠️",
|
|
iconBg: "bg-red-light/20",
|
|
},
|
|
warning: {
|
|
confirmBtn:
|
|
"bg-yellow-dark hover:bg-yellow-dark/90 focus:ring-yellow-dark",
|
|
icon: "⚠️",
|
|
iconBg: "bg-yellow-light/20",
|
|
},
|
|
};
|
|
|
|
const sizeStyles = {
|
|
sm: "max-w-sm",
|
|
md: "max-w-md",
|
|
lg: "max-w-lg",
|
|
};
|
|
|
|
const currentVariant = variantStyles[variant];
|
|
const currentSize = sizeStyles[size];
|
|
|
|
return createPortal(
|
|
<div
|
|
className="fixed inset-0 z-50 overflow-y-auto"
|
|
aria-labelledby="modal-title"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
>
|
|
<div
|
|
className="fixed inset-0 bg-dark/70 bg-opacity-75 transition-opacity"
|
|
onClick={onCancel}
|
|
/>
|
|
|
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
<div
|
|
className={`relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full ${currentSize} dark:bg-dark-2`}
|
|
>
|
|
<div className="bg-white px-4 pb-4 pt-5 dark:bg-dark-2 sm:p-6 sm:pb-4">
|
|
<div className="sm:flex sm:items-start">
|
|
<div
|
|
className={`mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full ${currentVariant.iconBg} sm:mx-0 sm:h-10 sm:w-10`}
|
|
>
|
|
<span className="text-xl">{currentVariant.icon}</span>
|
|
</div>
|
|
|
|
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
|
<h3
|
|
className="text-base font-semibold leading-6 text-dark dark:text-white"
|
|
id="modal-title"
|
|
>
|
|
{title}
|
|
</h3>
|
|
<div className="mt-2">
|
|
<p className="text-sm text-dark-5 dark:text-dark-6">
|
|
{message}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray px-4 py-3 dark:border-t dark:border-stroke-dark dark:bg-dark-2 sm:flex sm:flex-row-reverse sm:px-6">
|
|
<button
|
|
type="button"
|
|
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 sm:ml-3 sm:w-auto ${currentVariant.confirmBtn} dark:ring-offset-dark-2`}
|
|
disabled={isMutating > 0}
|
|
onClick={onConfirm}
|
|
>
|
|
{isMutating ? (
|
|
<div className="h-5 w-5 animate-spin rounded-full border-b-2 border-gray-1"></div>
|
|
) : (
|
|
confirmText
|
|
)}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-dark shadow-sm ring-1 ring-inset ring-stroke hover:bg-gray focus:outline-none focus:ring-2 focus:ring-dark-5 focus:ring-offset-2 dark:bg-dark-3 dark:text-white dark:ring-stroke-dark dark:hover:bg-dark-4 dark:focus:ring-offset-dark-2 sm:mt-0 sm:w-auto"
|
|
onClick={onCancel}
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
};
|
|
export default ConfirmationModal;
|