feat: add confirmation modal

This commit is contained in:
Aarnav Tale 2024-03-30 18:39:46 -04:00
parent 381c3d6df4
commit b146e4c3a8
No known key found for this signature in database
2 changed files with 146 additions and 45 deletions

128
app/components/Modal.tsx Normal file
View File

@ -0,0 +1,128 @@
import { Dialog, Transition } from '@headlessui/react'
import { XMarkIcon } from '@heroicons/react/24/outline'
import clsx from 'clsx'
import { Fragment, type SetStateAction, useState } from 'react'
import Button from './Button'
type HookParameters = {
title: string;
description?: string;
buttonText?: string;
variant?: 'danger' | 'confirm';
// Optional because the button submits
onConfirm?: () => void | Promise<void>;
}
type Properties = {
readonly isOpen: boolean;
readonly setIsOpen: (value: SetStateAction<boolean>) => void;
readonly parameters: HookParameters;
}
export default function useModal(properties: HookParameters) {
const [isOpen, setIsOpen] = useState(false)
return {
Modal: (
<Modal
isOpen={isOpen}
setIsOpen={setIsOpen}
parameters={properties}
/>
),
open: () => {
setIsOpen(true)
},
close: () => {
setIsOpen(false)
}
}
}
function Modal({ parameters, isOpen, setIsOpen }: Properties) {
return (
<Transition
show={isOpen}
as={Fragment}
>
<Dialog
as='div'
className='relative z-50'
onClose={() => {
setIsOpen(false)
}}
>
<Transition.Child
enter='ease-out duration-100'
enterFrom='opacity-0'
enterTo='opacity-100'
leave='ease-in duration-75'
leaveFrom='opacity-100'
leaveTo='opacity-0'
as={Fragment}
>
<div className='fixed inset-0 bg-black/30' aria-hidden='true'/>
</Transition.Child>
<div className='fixed inset-0 flex w-screen items-center justify-center'>
<Transition.Child
enter='transition ease-out duration-100'
enterFrom='transform opacity-0 scale-95'
enterTo='transform opacity-100 scale-100'
leave='transition ease-in duration-75'
leaveFrom='transform opacity-100 scale-100'
leaveTo='transform opacity-0 scale-95'
as={Fragment}
>
<Dialog.Panel className={clsx(
'rounded-lg p-4 w-full max-w-md',
'bg-white dark:bg-black relative',
'border border-gray-200 dark:border-zinc-800'
)}
>
<XMarkIcon
className={clsx(
'absolute top-3 right-3 rounded-lg p-1.5',
'w-8 h-8 text-gray-500 dark:text-gray-400',
'hover:bg-gray-100 dark:hover:bg-zinc-800'
)}
onClick={() => {
setIsOpen(false)
}}
/>
<Dialog.Title className='text-xl font-bold'>
{parameters.title}
</Dialog.Title>
{parameters.description ? (
<Dialog.Description className='text-gray-500 dark:text-gray-400 mt-1'>
{parameters.description}
</Dialog.Description>
) : undefined}
<Button
variant='emphasized'
type='submit'
className={clsx(
'w-full mt-12',
parameters.variant === 'danger'
? 'bg-red-800 dark:bg-red-500 focus:ring-red-500 dark:focus:ring-red-500'
: ''
)}
onClick={async () => {
if (parameters.onConfirm) {
await parameters.onConfirm()
}
setIsOpen(false)
}}
>
{parameters.buttonText ?? 'Confirm'}
</Button>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
)
}

View File

@ -1,9 +1,7 @@
import { Dialog } from '@headlessui/react'
import { useFetcher } from '@remix-run/react'
import clsx from 'clsx'
import { useState } from 'react'
import Button from '~/components/Button'
import useModal from '~/components/Modal'
import Spinner from '~/components/Spinner'
type Properties = {
@ -13,8 +11,22 @@ type Properties = {
}
export default function Modal({ isEnabled, disabled }: Properties) {
const [isOpen, setIsOpen] = useState(false)
const fetcher = useFetcher()
const { Modal, open } = useModal({
title: `${isEnabled ? 'Disable' : 'Enable'} Magic DNS`,
variant: isEnabled ? 'danger' : 'confirm',
buttonText: `${isEnabled ? 'Disable' : 'Enable'} Magic DNS`,
description: 'Devices will no longer be accessible via your tailnet domain. The search domain will also be disabled.',
onConfirm: () => {
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.magic_dns': !isEnabled
}, {
method: 'PATCH',
encType: 'application/json'
})
}
})
return (
<>
@ -23,7 +35,7 @@ export default function Modal({ isEnabled, disabled }: Properties) {
className='w-fit text-sm'
disabled={disabled}
onClick={() => {
setIsOpen(true)
open()
}}
>
{fetcher.state === 'idle' ? undefined : (
@ -31,46 +43,7 @@ export default function Modal({ isEnabled, disabled }: Properties) {
)}
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Button>
<Dialog
className='relative z-50'
open={isOpen} onClose={() => {
setIsOpen(false)
}}
>
<div className='fixed inset-0 bg-black/30' aria-hidden='true'/>
<div className='fixed inset-0 flex w-screen items-center justify-center'>
<Dialog.Panel className='bg-white rounded-lg p-4 w-full max-w-md'>
<Dialog.Title className='text-lg font-bold'>
{isEnabled ? 'Disable' : 'Enable'} Magic DNS
</Dialog.Title>
<Dialog.Description className='text-gray-500 dark:text-gray-400'>
Devices will no longer be accessible via your tailnet domain.
The search domain will also be disabled.
</Dialog.Description>
<Button
variant='emphasized'
type='submit'
className={clsx(
'w-full mt-12',
isEnabled ? 'bg-red-800 dark:bg-red-500' : ''
)}
onClick={() => {
fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.magic_dns': !isEnabled
}, {
method: 'PATCH',
encType: 'application/json'
})
setIsOpen(false)
}}
>
{isEnabled ? 'Disable' : 'Enable'}
</Button>
</Dialog.Panel>
</div>
</Dialog>
{Modal}
</>
)
}