294 lines
12 KiB
TypeScript
294 lines
12 KiB
TypeScript
// frontend\src\components\Modal.tsx
|
|
|
|
import type { ReactNode } from 'react'
|
|
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react'
|
|
import {
|
|
CheckIcon,
|
|
ExclamationTriangleIcon,
|
|
XMarkIcon,
|
|
} from '@heroicons/react/24/outline'
|
|
|
|
type ModalVariant =
|
|
| 'success-single'
|
|
| 'success-wide'
|
|
| 'alert'
|
|
| 'alert-dismiss'
|
|
| 'alert-gray-footer'
|
|
| 'alert-left-buttons'
|
|
|
|
type ModalProps = {
|
|
open: boolean
|
|
setOpen: (open: boolean) => void
|
|
variant?: ModalVariant
|
|
title?: string
|
|
description?: string
|
|
confirmText?: string
|
|
cancelText?: string
|
|
onConfirm?: () => void
|
|
|
|
children?: ReactNode
|
|
panelClassName?: string
|
|
zIndexClassName?: string
|
|
}
|
|
|
|
function classNames(...classes: Array<string | false | null | undefined>) {
|
|
return classes.filter(Boolean).join(' ')
|
|
}
|
|
|
|
export default function Modal({
|
|
open,
|
|
setOpen,
|
|
variant = 'success-single',
|
|
title,
|
|
description,
|
|
confirmText,
|
|
cancelText = 'Cancel',
|
|
onConfirm,
|
|
children,
|
|
panelClassName,
|
|
zIndexClassName = 'z-10',
|
|
}: ModalProps) {
|
|
const isSuccess = variant === 'success-single' || variant === 'success-wide'
|
|
const isGrayFooter = variant === 'alert-gray-footer'
|
|
const isDismiss = variant === 'alert-dismiss'
|
|
const isLeftButtons = variant === 'alert-left-buttons'
|
|
const isWideSuccess = variant === 'success-wide'
|
|
|
|
const Icon = isSuccess ? CheckIcon : ExclamationTriangleIcon
|
|
|
|
const resolvedTitle =
|
|
title ?? (isSuccess ? 'Payment successful' : 'Deactivate account')
|
|
|
|
const resolvedDescription =
|
|
description ??
|
|
(isSuccess
|
|
? 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Consequatur amet labore.'
|
|
: 'Are you sure you want to deactivate your account? All of your data will be permanently removed from our servers forever. This action cannot be undone.')
|
|
|
|
const resolvedConfirmText =
|
|
confirmText ?? (isSuccess ? 'Go back to dashboard' : 'Deactivate')
|
|
|
|
function handleConfirm() {
|
|
onConfirm?.()
|
|
setOpen(false)
|
|
}
|
|
|
|
if (children) {
|
|
return (
|
|
<Dialog open={open} onClose={setOpen} className={classNames('relative', zIndexClassName)}>
|
|
<DialogBackdrop
|
|
transition
|
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-gray-900/50"
|
|
/>
|
|
|
|
<div className="fixed inset-0 z-10 w-screen overflow-hidden">
|
|
<div className="flex h-full items-center justify-center p-4 text-center">
|
|
<DialogPanel
|
|
transition
|
|
className={classNames(
|
|
'relative w-full transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 data-closed:sm:translate-y-0 data-closed:sm:scale-95 dark:bg-gray-800 dark:outline dark:-outline-offset-1 dark:outline-white/10',
|
|
panelClassName,
|
|
)}
|
|
>
|
|
{children}
|
|
</DialogPanel>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
if (isGrayFooter) {
|
|
return (
|
|
<Dialog open={open} onClose={setOpen} className="relative z-10">
|
|
<DialogBackdrop
|
|
transition
|
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-gray-900/50"
|
|
/>
|
|
|
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
<DialogPanel
|
|
transition
|
|
className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:max-w-lg data-closed:sm:translate-y-0 data-closed:sm:scale-95 dark:bg-gray-800 dark:outline dark:-outline-offset-1 dark:outline-white/10"
|
|
>
|
|
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 dark:bg-gray-800">
|
|
<div className="sm:flex sm:items-start">
|
|
<div className="mx-auto flex size-12 shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:size-10 dark:bg-red-500/10">
|
|
<Icon aria-hidden="true" className="size-6 text-red-600 dark:text-red-400" />
|
|
</div>
|
|
|
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
|
<DialogTitle as="h3" className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{resolvedTitle}
|
|
</DialogTitle>
|
|
|
|
<div className="mt-2">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{resolvedDescription}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 dark:bg-gray-700/25">
|
|
<button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
className="inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 sm:ml-3 sm:w-auto dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400"
|
|
>
|
|
{resolvedConfirmText}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
data-autofocus
|
|
onClick={() => setOpen(false)}
|
|
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring inset-ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
</div>
|
|
</DialogPanel>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Dialog open={open} onClose={setOpen} className="relative z-10">
|
|
<DialogBackdrop
|
|
transition
|
|
className="fixed inset-0 bg-gray-500/75 transition-opacity data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in dark:bg-gray-900/50"
|
|
/>
|
|
|
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
|
<DialogPanel
|
|
transition
|
|
className={classNames(
|
|
'relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all data-closed:translate-y-4 data-closed:opacity-0 data-enter:duration-300 data-enter:ease-out data-leave:duration-200 data-leave:ease-in sm:my-8 sm:w-full sm:p-6 data-closed:sm:translate-y-0 data-closed:sm:scale-95 dark:bg-gray-800 dark:outline dark:-outline-offset-1 dark:outline-white/10',
|
|
isSuccess && !isWideSuccess ? 'sm:max-w-sm' : 'sm:max-w-lg',
|
|
)}
|
|
>
|
|
{isDismiss && (
|
|
<div className="absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(false)}
|
|
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-2 focus:outline-offset-2 focus:outline-indigo-600 dark:bg-gray-800 dark:hover:text-gray-300 dark:focus:outline-white"
|
|
>
|
|
<span className="sr-only">Close</span>
|
|
<XMarkIcon aria-hidden="true" className="size-6" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className={classNames(!isSuccess && 'sm:flex sm:items-start')}>
|
|
<div
|
|
className={classNames(
|
|
'mx-auto flex size-12 shrink-0 items-center justify-center rounded-full',
|
|
isSuccess
|
|
? 'bg-green-100 dark:bg-green-500/10'
|
|
: 'bg-red-100 sm:mx-0 sm:size-10 dark:bg-red-500/10',
|
|
)}
|
|
>
|
|
<Icon
|
|
aria-hidden="true"
|
|
className={classNames(
|
|
'size-6',
|
|
isSuccess ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400',
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
className={classNames(
|
|
'mt-3 text-center',
|
|
isSuccess ? 'sm:mt-5' : 'sm:mt-0 sm:ml-4 sm:text-left',
|
|
)}
|
|
>
|
|
<DialogTitle as="h3" className="text-base font-semibold text-gray-900 dark:text-white">
|
|
{resolvedTitle}
|
|
</DialogTitle>
|
|
|
|
<div className="mt-2">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{resolvedDescription}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{variant === 'success-single' && (
|
|
<div className="mt-5 sm:mt-6">
|
|
<button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
className="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
|
|
>
|
|
{resolvedConfirmText}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{variant === 'success-wide' && (
|
|
<div className="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3">
|
|
<button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
className="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 sm:col-start-2 dark:bg-indigo-500 dark:shadow-none dark:hover:bg-indigo-400 dark:focus-visible:outline-indigo-500"
|
|
>
|
|
{resolvedConfirmText}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
data-autofocus
|
|
onClick={() => setOpen(false)}
|
|
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 sm:col-start-1 sm:mt-0 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!isSuccess && (
|
|
<div
|
|
className={classNames(
|
|
'mt-5 sm:mt-4',
|
|
isLeftButtons ? 'sm:ml-10 sm:flex sm:pl-4' : 'sm:flex sm:flex-row-reverse',
|
|
)}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={handleConfirm}
|
|
className={classNames(
|
|
'inline-flex w-full justify-center rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-red-500 dark:bg-red-500 dark:shadow-none dark:hover:bg-red-400',
|
|
isLeftButtons ? 'sm:w-auto' : 'sm:ml-3 sm:w-auto',
|
|
)}
|
|
>
|
|
{resolvedConfirmText}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
data-autofocus
|
|
onClick={() => setOpen(false)}
|
|
className={classNames(
|
|
'mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20',
|
|
isLeftButtons && 'sm:ml-3',
|
|
)}
|
|
>
|
|
{cancelText}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</DialogPanel>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
)
|
|
} |